Teams usually adopt internal documentation after incidents: a migration that broke because one ENV var was undocumented, an on-call handoff with no runbook, or a recurring deployment checklist stored in someone’s private notes. BookStack is a practical answer when you need a searchable internal knowledge base with role-based access, structured books/chapters/pages, and predictable self-hosted operations. This guide shows a production-ready deployment of BookStack on Ubuntu using Docker Compose, Traefik as the reverse proxy, and MariaDB as the backend database. The focus is operational reliability: secret handling, network isolation, TLS routing, backups, upgrades, and failure recovery steps you can execute under pressure.
We’ll deploy BookStack as a non-root service user, keep credentials out of Compose files, enforce least-privilege networking, and add validation checks so you can prove the stack is healthy before onboarding users. The same pattern works well for internal IT wikis, engineering runbooks, customer-support playbooks, and security documentation where controlled sharing and auditability matter.
Architecture and flow overview
The deployment uses three containers on a dedicated Docker network: bookstack, mariadb, and the existing traefik edge proxy. Traefik terminates TLS, routes your hostname to the BookStack service, and applies security headers/middlewares if configured. BookStack talks to MariaDB over an internal network only; the database port is never exposed publicly. Persistent data is stored in named volumes for both MariaDB data files and BookStack uploads.
Request flow is straightforward: user browser → HTTPS on Traefik entrypoint → router rule for your BookStack host → BookStack container on internal port 80 → MariaDB over internal network. This separation keeps blast radius small and simplifies upgrades: you can roll application images forward while keeping stateful data untouched. For disaster recovery, backups include MariaDB dumps plus uploads/config volumes, then are verified by a restore drill in a staging environment.
Prerequisites
- Ubuntu 22.04/24.04 host with Docker Engine and Docker Compose plugin installed
- A working Traefik instance with HTTPS entrypoint (for example
websecure) - A DNS record for your docs hostname (example:
docs.example.com) pointing to the Traefik host - At least 2 vCPU, 4 GB RAM, and fast persistent disk (SSD recommended)
- Shell access with sudo privileges
Before starting, decide where this stack lives (for example /opt/bookstack) and who owns it. Using a dedicated non-root UNIX account keeps file permissions cleaner and avoids accidental root-owned artifacts during maintenance.
Step-by-step deployment
1) Prepare host directories and service account
sudo useradd --system --create-home --home-dir /opt/bookstack --shell /usr/sbin/nologin bookstack
sudo mkdir -p /opt/bookstack/{config,backups}
sudo chown -R bookstack:bookstack /opt/bookstack
If the copy button does not work in your browser/editor, manually select the command block and copy.
Using a system account avoids coupling this service to a human user account and makes ownership explicit. Keep all runtime files in one path so backups and restores remain deterministic.
2) Create environment file with strong secrets
sudo -u bookstack bash -lc 'cd /opt/bookstack && cat > .env <<EOF
APP_URL=https://docs.example.com
APP_KEY=
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=bookstack
DB_USERNAME=bookstack
DB_PASSWORD=CHANGE_ME_DB_PASSWORD
MYSQL_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD
PUID=1000
PGID=1000
TZ=UTC
EOF'
If the copy button does not work in your browser/editor, manually select the command block and copy.
Set real passwords before first boot. Keep this file readable only by the service owner. We’ll generate an app key after first start and write it back into .env.
3) Define Docker Compose stack
sudo -u bookstack bash -lc 'cd /opt/bookstack && cat > docker-compose.yml <<EOF
services:
mariadb:
image: mariadb:11.4
container_name: bookstack-mariadb
restart: unless-stopped
env_file: .env
environment:
- MARIADB_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MARIADB_DATABASE=${DB_DATABASE}
- MARIADB_USER=${DB_USERNAME}
- MARIADB_PASSWORD=${DB_PASSWORD}
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
volumes:
- mariadb_data:/var/lib/mysql
networks:
- bookstack_internal
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 20s
timeout: 5s
retries: 10
bookstack:
image: lscr.io/linuxserver/bookstack:latest
container_name: bookstack-app
restart: unless-stopped
depends_on:
mariadb:
condition: service_healthy
env_file: .env
volumes:
- ./config:/config
networks:
- bookstack_internal
- traefik_proxy
labels:
- traefik.enable=true
- traefik.docker.network=traefik_proxy
- traefik.http.routers.bookstack.rule=Host(`docs.example.com`)
- traefik.http.routers.bookstack.entrypoints=websecure
- traefik.http.routers.bookstack.tls=true
- traefik.http.services.bookstack.loadbalancer.server.port=80
networks:
bookstack_internal:
driver: bridge
traefik_proxy:
external: true
volumes:
mariadb_data:
EOF'
If the copy button does not work in your browser/editor, manually select the command block and copy.
This pattern keeps MariaDB private while allowing BookStack to join Traefik’s external network for ingress. If your Traefik network name differs, update traefik_proxy and the label accordingly.
4) Bring up the stack and initialize application key
sudo -u bookstack bash -lc 'cd /opt/bookstack && docker compose pull && docker compose up -d'
sudo -u bookstack bash -lc 'cd /opt/bookstack && docker compose exec -T bookstack php /app/www/artisan key:generate --force --no-interaction'
If the copy button does not work in your browser/editor, manually select the command block and copy.
After key generation, confirm APP_KEY is set in runtime config. If key generation fails due to startup timing, wait 15–20 seconds and retry once.
5) Create least-privilege backup routine
sudo -u bookstack bash -lc 'cd /opt/bookstack && cat > backups/backup_bookstack.sh <<EOF
#!/usr/bin/env bash
set -euo pipefail
cd /opt/bookstack
source .env
STAMP=$(date +%F-%H%M%S)
mkdir -p backups/$STAMP
docker compose exec -T mariadb sh -lc "mysqldump -u$DB_USERNAME -p$DB_PASSWORD $DB_DATABASE" > backups/$STAMP/bookstack.sql
cp -a config backups/$STAMP/config
tar -czf backups/bookstack-backup-$STAMP.tar.gz -C backups $STAMP
rm -rf backups/$STAMP
EOF
chmod +x backups/backup_bookstack.sh'
If the copy button does not work in your browser/editor, manually select the command block and copy.
Backups are useless until restored successfully. Later in this guide we include a restore validation step so your runbook is evidence-based, not assumption-based.
6) Add scheduled backups with retention
sudo crontab -u bookstack -e
# Add:
# 30 2 * * * /opt/bookstack/backups/backup_bookstack.sh
# 45 2 * * * find /opt/bookstack/backups -name "bookstack-backup-*.tar.gz" -mtime +14 -delete
If the copy button does not work in your browser/editor, manually select the command block and copy.
Use your organization’s retention policy (for example 14, 30, or 90 days) and ship backups off-host if ransomware resilience is a requirement.
Configuration and secrets handling best practices
Keep secrets out of Git: never commit .env, backup archives, or SQL dumps. Add strict ignore rules and check CI logs for accidental secret exposure. Use secret rotation windows: rotate DB credentials after major incidents, contractor offboarding, or environment cloning. Prefer explicit trust boundaries: only BookStack should reach MariaDB; no host-level port mapping is necessary for the database.
Harden HTTP behavior in Traefik: enforce HTTPS redirection, HSTS (if policy allows), and upstream timeouts that prevent slowloris-style resource exhaustion. If your environment supports middleware chains, add basic bot/rate controls for brute-force reduction. Set conservative upload limits: if your teams upload diagrams and logs, size limits should be intentional to avoid disk spikes. Also monitor inode and disk growth in both /opt/bookstack/config and Docker volumes.
Operational ownership: define who is accountable for backup health, certificate expiry response, and post-upgrade validation. Documentation platforms fail socially when ownership is ambiguous; assign an on-call group and include rollback instructions in your internal runbook.
Verification checklist
Run these checks before announcing the service:
# Containers healthy
sudo -u bookstack bash -lc 'cd /opt/bookstack && docker compose ps'
# App reachable through Traefik TLS endpoint
curl -I https://docs.example.com
# Inspect logs for boot/database errors
sudo -u bookstack bash -lc 'cd /opt/bookstack && docker compose logs --since=10m bookstack mariadb'
# Validate backup script executes
sudo -u bookstack /opt/bookstack/backups/backup_bookstack.sh
If the copy button does not work in your browser/editor, manually select the command block and copy.
Then perform a restore drill quarterly. Restore one backup into an isolated host/project, confirm login works, verify at least one recent page/attachment is present, and document the real RTO/RPO achieved.
Common issues and fixes
BookStack shows 502/504 behind Traefik
Usually this is a network mismatch or wrong service port label. Confirm traefik.docker.network matches the external network and that BookStack service label points to port 80. Also check container-level health and restart loops.
Database connection errors at startup
Most failures come from mismatched credentials or DB not yet healthy. Verify DB_HOST, DB_USERNAME, and DB_PASSWORD in .env; then ensure MariaDB healthcheck passed before app start. If credentials were changed manually, restart both services after updating env values.
Login/session problems after hostname change
Set APP_URL exactly to the new public HTTPS URL and restart the app container. Session and URL-generation issues often trace to stale APP_URL values when moving from staging to production hostname.
Backup archive exists but restore fails
This usually means backup completeness was assumed, not verified. Confirm the SQL dump is non-empty and includes tables, and that config contents were captured. Run a scripted restore test monthly to detect corruption, permission drift, or format changes early.
FAQ
Can I run BookStack without Traefik?
Yes, but you still need a reliable TLS termination layer (Nginx, Caddy, or cloud load balancer). This guide uses Traefik because it integrates cleanly with Docker labels and multi-service routing.
Why MariaDB instead of SQLite for small teams?
MariaDB is more predictable for concurrent usage, backup tooling, and growth. Even modest teams benefit from consistent operations and simpler migration paths as usage increases.
How do I rotate database passwords safely?
Create a short maintenance window, update credentials in MariaDB and .env, restart containers, then validate application login and write actions. Keep previous credentials only for rollback and remove promptly.
What is the minimum backup policy for production?
Daily automated backups, retention aligned with business policy, and regular restore drills. If compliance requires it, add immutable or off-site copies and monitor backup job failures as alerting events.
How should I handle upgrades?
Pull images in staging first, run smoke tests, capture a backup, then upgrade production during a low-risk window. Keep a rollback plan with known-good image tags and restore checkpoints.
Can I integrate SSO?
Yes. Many teams front BookStack with SSO-aware proxies or identity layers. Validate user provisioning, logout behavior, and role mapping carefully before enforcing SSO for all users.
How do I reduce attack surface further?
Limit exposed ports, enforce strong auth policies, segment networks, monitor access logs, and patch regularly. Pair this with host hardening and vulnerability scanning for container images.
Related internal guides
- Deploy DocuSeal with Docker Compose + Traefik + PostgreSQL
- Deploy NocoDB with Docker Compose + Traefik + PostgreSQL
- Deploy Vaultwarden with Docker Compose + Caddy + PostgreSQL
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.