Skip to main content

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 a ReadWriteMany storage 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.
  • /config on the PVC holds uploads, themes, logs, and the generated .env — back it up the same way you'd back up the database.