Every team accumulates knowledge that lives in the wrong place - a pinned Slack message, a Google Doc nobody can find, a README three repos deep, someone's memory. A wiki fixes that, but the hosted options either lock your content behind a per-seat subscription or bury a simple "how do we deploy this" page under a project-management suite you never wanted.
BookStack is the antidote. It's a free, open-source documentation platform with a deliberately simple structure - shelves hold books, books hold chapters, chapters hold pages - plus a clean WYSIWYG editor, full-text search, page revisions, and per-item permissions. It runs happily on a 1 GB VPS and stores everything in a MariaDB database you control. This guide takes you from a bare Ubuntu server to a working wiki.example.com with HTTPS, a login, and automated backups.
wiki.example.com at the serverlinuxserver/bookstack plus MariaDB behind Caddy in one docker-compose.ymlAPP_KEY, set APP_URL to your real HTTPS URLTotal time: about 30 minutes.
80 and 443 open to the internet for Let's EncryptBookStack itself is a PHP app and sits around 150-250 MB of RAM. The MariaDB database adds another 100-200 MB. A 1 GB box is comfortable for a small team; bump to 2 GB if you expect dozens of concurrent editors or a large image library.
Quick orientation, because the field is crowded:
The structure is the real selling point. Shelves, books, chapters, and pages map to how people actually think about documentation, so nobody has to invent a folder taxonomy on day one.
In your DNS provider, add an A record:
wiki.example.com → YOUR_VPS_IPV4
Add an AAAA record if you use IPv6. Verify it resolves:
dig +short wiki.example.com
DNS has to point at your VPS before Caddy can fetch a certificate.
On a fresh Ubuntu box:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Confirm:
docker --version
docker compose version
If you use UFW:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Caddy needs port 80 for the ACME HTTP challenge and 443 for HTTPS. The database is never exposed to the host - it only talks to BookStack over the internal Docker network.
sudo mkdir -p /opt/bookstack
cd /opt/bookstack
sudo mkdir -p bookstack-app-data bookstack-db caddy-data caddy-config
Everything persistent lives under /opt/bookstack. Back up this one directory plus a database dump and you can rebuild the stack on any host.
BookStack encrypts session data and some stored values with a Laravel APP_KEY. Generate one before you write the compose file:
docker run --rm --entrypoint /generate-key.sh lscr.io/linuxserver/bookstack:latest
It prints something like base64:Zt2q...==. Copy the whole string, including the base64: prefix - you'll paste it in the next step.
Create /opt/bookstack/docker-compose.yml. Fill in the two passwords and the APP_KEY you just generated:
services:
bookstack:
image: lscr.io/linuxserver/bookstack:24.05
container_name: bookstack
restart: unless-stopped
depends_on:
- bookstack-db
environment:
PUID: "1000"
PGID: "1000"
TZ: Europe/Berlin
APP_URL: https://wiki.example.com
APP_KEY: base64:PASTE_YOUR_GENERATED_KEY_HERE
DB_HOST: bookstack-db
DB_PORT: "3306"
DB_DATABASE: bookstack
DB_USERNAME: bookstack
DB_PASSWORD: CHANGE_ME_APP_DB_PASSWORD
volumes:
- ./bookstack-app-data:/config
networks:
- bookstack-net
bookstack-db:
image: lscr.io/linuxserver/mariadb:11.4
container_name: bookstack-db
restart: unless-stopped
environment:
PUID: "1000"
PGID: "1000"
TZ: Europe/Berlin
MYSQL_ROOT_PASSWORD: CHANGE_ME_ROOT_PASSWORD
MYSQL_DATABASE: bookstack
MYSQL_USER: bookstack
MYSQL_PASSWORD: CHANGE_ME_APP_DB_PASSWORD
volumes:
- ./bookstack-db:/config
networks:
- bookstack-net
caddy:
image: caddy:2
container_name: bookstack-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
networks:
- bookstack-net
networks:
bookstack-net:
A few notes:
APP_URL must exactly match the public HTTPS URL, scheme included. BookStack builds every link, asset path, and email against it. Set it to http:// or the wrong hostname and you get broken CSS, redirect loops, and dead reset links.DB_PASSWORD in the app service and MYSQL_PASSWORD in the database service must be identical - that's the account BookStack logs in with. MYSQL_ROOT_PASSWORD is separate and only used for admin tasks.80 internally. We don't publish it on the host because Caddy reaches it on the Docker network.latest so an unattended docker compose pull can't jump you across a major MariaDB or BookStack release unannounced.Create /opt/bookstack/Caddyfile:
wiki.example.com {
encode zstd gzip
reverse_proxy bookstack:80
}
Caddy requests a Let's Encrypt certificate for wiki.example.com automatically on first boot. No extra config needed.
cd /opt/bookstack
sudo docker compose up -d
sudo docker compose logs -f
The first boot takes a minute or two - MariaDB initializes its data directory, then BookStack runs its database migrations against it. Watch until Caddy reports the certificate was issued and BookStack logs show the app is ready.
Open https://wiki.example.com in a browser. BookStack ships with a default administrator account:
[email protected]passwordLog in, then immediately:
Leaving the default admin credentials on a public URL is the single most common way self-hosted BookStack instances get compromised. Do this before you do anything else.
Head to Settings (gear icon → Settings) and configure the basics:
For a team, you invite people under Settings → Users → Add New User, set a temporary password, and let them change it on first login. Assign each user to a role - BookStack ships Admin, Editor, and Viewer roles out of the box, and you can create custom roles with granular permissions per shelf or book.
BookStack's hierarchy is worth understanding before you start typing:
Start with one shelf and one book. Click Shelves → Create New Shelf, name it, then create a book inside it and add your first page. The editor supports a Markdown mode too - toggle it under your profile preferences if you'd rather type than click. Both modes save to the same page, so your team can mix and match.
Every page keeps a full revision history, so edits are never destructive - you can diff and roll back any change from the page's Revisions tab.
Two things matter: the MariaDB database (all your text, structure, users, and permissions) and the app data volume (uploaded images and attachments). A tar of the volumes alone is not a safe database backup - you need a proper SQL dump.
Create /usr/local/bin/bookstack-backup.sh:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/bookstack"
DATE="$(date +%F-%H%M)"
mkdir -p "$BACKUP_DIR"
docker exec bookstack-db \
mariadb-dump -u bookstack -p"CHANGE_ME_APP_DB_PASSWORD" bookstack \
| gzip > "$BACKUP_DIR/bookstack-db-$DATE.sql.gz"
tar -czf "$BACKUP_DIR/bookstack-files-$DATE.tar.gz" \
-C /opt/bookstack bookstack-app-data
find "$BACKUP_DIR" -name "bookstack-*.gz" -mtime +14 -delete
Make it executable and schedule a daily run:
sudo chmod +x /usr/local/bin/bookstack-backup.sh
echo "30 3 * * * root /usr/local/bin/bookstack-backup.sh" | \
sudo tee /etc/cron.d/bookstack-backup
For off-site safety, sync /var/backups/bookstack to S3, Backblaze B2, or another VPS with rclone on the same schedule. Our restic to S3 guide covers a hardened, encrypted version of exactly this.
The linuxserver image applies BookStack's database migrations on startup, so upgrading is a tag bump plus a pull:
cd /opt/bookstack
# take a fresh backup first
sudo /usr/local/bin/bookstack-backup.sh
# bump the image tags in docker-compose.yml, then:
sudo docker compose pull
sudo docker compose up -d
Always back up before pulling. Migrations run automatically and rolling back a half-migrated database is far easier from a known-good dump than by hand. Bump BookStack and MariaDB one major version at a time - skipping several at once is where upgrades go wrong.
An internal team wiki is a strong candidate for VPN-only access. Pair this with our Tailscale guide so wiki.example.com only resolves inside your tailnet and never touches the public internet. BookStack works fine over WireGuard or Tailscale, and you remove the public attack surface entirely - no default-credential scanners, no bots. If you'd rather keep it public but locked down, our SSH hardening and fail2ban guide covers the server side.
Broken CSS, redirect loops, or dead password-reset links. APP_URL doesn't match the address you're loading. Set it to the exact https://wiki.example.com you use in the browser, then recreate the container with sudo docker compose up -d.
Caddy returns 502 Bad Gateway. BookStack hasn't finished starting or crashed on a bad APP_KEY. Check sudo docker compose logs bookstack. If you see a decryption or "unsupported cipher" error, your APP_KEY is missing the base64: prefix or was truncated.
"Access denied for user 'bookstack'." The DB_PASSWORD in the app service and MYSQL_PASSWORD in the database service don't match. Fix both to the same value. Note that changing MYSQL_PASSWORD after the database is initialized has no effect - it's only read on first init - so if they drifted, reset it inside the running database or recreate the empty bookstack-db volume before you have content.
Can't log in with [email protected]. You already changed it (good) or a previous run created the admin. Reset any user's password from the host with sudo docker exec -it bookstack php artisan bookstack:reset-mfa and the related reset commands, or edit the user directly in the database.
Uploaded images 404 after a restore. You restored the database but not the bookstack-app-data volume. Both halves of the backup are required - the SQL dump references files that live on disk.
MAIL_* environment variables to your SMTP provider so BookStack can send user invites and password resets. Our Postfix SMTP relay guide is one way to get reliable delivery.Self-hosted BookStack is the rare tool your team actually keeps using. It stays out of the way, the structure makes sense to newcomers, and the knowledge that used to evaporate into chat history now has a permanent home you own.
Need a small VPS to run a stack like this? Our Linux plans include fast NVMe storage, IPv6, and plenty of headroom for Docker workloads. See the options.