New Page
BookStack on Kubernetes (in-cluster MySQL + external HAProxy)
Image used: lscr.io/linuxserver/bookstack:latest (community-maintained, actively updated, includes /status health endpoint that BookStack's own HA docs recommend probing).
1. Create the database in your existing MySQL
BookStack manages its own schema, so it just needs an empty database + a user with full rights on it. Run on your existing MySQL:
CREATE DATABASE bookstack CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'bookstack'@'%' IDENTIFIED BY 'CHANGE_ME_strong_password';
GRANT ALL PRIVILEGES ON bookstack.* TO 'bookstack'@'%';
FLUSH PRIVILEGES;
Use the same password in 01-secret.yaml (DB_PASSWORD).
2. Edit before applying
| File | What to change |
|---|---|
01-secret.yaml |
DB_PASSWORD to match what you set in MySQL |
02-configmap.yaml |
APP_URL (must match the host users/HAProxy hit), DB_HOST (your MySQL Service DNS name) |
03-pvc.yaml |
storageClassName — run kubectl get storageclass to see what's available |
06-ingress.yaml |
host, and ingressClassName if you're not on ingress-nginx |
DB_HOST resolution inside the cluster:
- Same namespace as MySQL's Service → just the Service name, e.g.
mysql - Different namespace →
mysql.<that-namespace>.svc.cluster.local
3. Apply
kubectl apply -f 00-namespace.yaml
kubectl apply -f 01-secret.yaml
kubectl apply -f 02-configmap.yaml
kubectl apply -f 03-pvc.yaml
kubectl apply -f 04-deployment.yaml
kubectl apply -f 05-service.yaml
kubectl apply -f 06-ingress.yaml
Check it's healthy:
kubectl -n bookstack get pods
kubectl -n bookstack logs -f deploy/bookstack
kubectl -n bookstack port-forward svc/bookstack 8080:80
curl -i http://localhost:8080/status # should return 200
Default login is admin@admin.com / password — change it immediately after first login.
4. Point HAProxy at it
This setup gives you a ClusterIP Service fronted by an Ingress. HAProxy outside the cluster can't reach a ClusterIP directly — it needs to hit something exposed on the node, which is your ingress controller's NodePort (or LoadBalancer IP if you have one).
Find the ingress controller's NodePort:
kubectl -n <ingress-namespace> get svc <ingress-controller-svc>
Example HAProxy backend, assuming ingress-nginx is exposed on NodePort 30080 across your worker nodes:
frontend bookstack_fe
bind *:80
mode http
default_backend bookstack_be
backend bookstack_be
mode http
balance roundrobin
option httpchk GET /status
http-check expect status 200
server node1 10.0.0.11:30080 check
server node2 10.0.0.12:30080 check
server node3 10.0.0.13:30080 check
http-request set-header Host bookstack.yourdomain.local
The set-header Host line matters — your Ingress rule routes by hostname, so HAProxy must forward the same Host header that's set in 06-ingress.yaml. If you terminate TLS at HAProxy and it's not the same as APP_URL's scheme, also set X-Forwarded-Proto.
Things to know
- Single replica only, as configured. BookStack's own HA guidance notes that sessions, cache, and uploaded files default to local filesystem. The PVC here is
ReadWriteOnce, so it can only be mounted by one pod. Scaling to multiple replicas would need aReadWriteManystorage class (e.g. NFS) for the PVC, plus moving session/cache to your DB or Redis — only worth it if you actually need HA. - APP_URL changes after first install require running
kubectl exec -it deploy/bookstack -n bookstack -- php /app/www/artisan bookstack:update-url <OLD> <NEW>to fix stored URLs in the database — don't just edit the ConfigMap and restart. /configon the PVC holds uploads, themes, logs, and the generated.env— back it up the same way you'd back up the database.