Self-Host BookStack on a VPS for a Private Wiki
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.
TL;DR
- Install Docker and Docker Compose on a small VPS
- Point a subdomain like
wiki.example.comat the server - Run
linuxserver/bookstackplus MariaDB behind Caddy in onedocker-compose.yml - Generate an
APP_KEY, setAPP_URLto your real HTTPS URL - Log in as the default admin, change the password immediately
- Back up the database and the uploads volume daily
Total time: about 30 minutes.
What You Need
- A VPS with at least 1 GB RAM running Ubuntu 22.04 or 24.04
- A domain or subdomain you can point at the server
- Ports
80and443open to the internet for Let's Encrypt - Root or sudo access
BookStack 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.
Why BookStack Over a Hosted Wiki
Quick orientation, because the field is crowded:
- Hosted wikis (Notion, Confluence, GitBook) are polished but rented. They bill per seat, your content lives on someone else's servers, and exporting everything cleanly when you leave is rarely painless.
- BookStack is a single app you own. No seat limits, no telemetry, no export lock-in - it does WYSIWYG or Markdown, ships an HTML/PDF/Markdown export on every page, and has a full REST API. The trade is that you run the updates and backups yourself, which on a VPS you already pay for is close to free.
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.
Step 1: Point a Subdomain at Your VPS
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.
Step 2: Install Docker and Docker Compose
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
Step 3: Open the Firewall
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.
Step 4: Create the Project Directory
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.
Step 5: Generate the APP_KEY
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.
Step 6: Write the Compose File
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_URLmust exactly match the public HTTPS URL, scheme included. BookStack builds every link, asset path, and email against it. Set it tohttp://or the wrong hostname and you get broken CSS, redirect loops, and dead reset links.DB_PASSWORDin the app service andMYSQL_PASSWORDin the database service must be identical - that's the account BookStack logs in with.MYSQL_ROOT_PASSWORDis separate and only used for admin tasks.- The BookStack container serves on port
80internally. We don't publish it on the host because Caddy reaches it on the Docker network. - Pin real version tags rather than
latestso an unattendeddocker compose pullcan't jump you across a major MariaDB or BookStack release unannounced.
Step 7: Write the Caddyfile
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.
Step 8: Start the Stack
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.
Step 9: Log In and Lock Down the Admin
Open https://wiki.example.com in a browser. BookStack ships with a default administrator account:
- Email:
[email protected] - Password:
password
Log in, then immediately:
- Click your avatar (top right) → Edit Profile.
- Change the email to your real address and set a strong password.
- Save, and log back in with the new credentials.
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.
Step 10: Set the Instance Name and Registration Policy
Head to Settings (gear icon → Settings) and configure the basics:
- Application Name - shows in the header and page titles.
- Allow public access - leave off unless you want anonymous readers. Off means every page requires a login.
- Allow registration - leave off for a private team wiki. You'll invite users by hand, which keeps random signups out.
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.
Step 11: Create Your First Shelf and Book
BookStack's hierarchy is worth understanding before you start typing:
- Shelf - a top-level grouping, like "Engineering" or "Company Handbook."
- Book - a collection of related pages, like "Deployment Runbooks."
- Chapter - an optional grouping of pages inside a book.
- Page - the actual content, written in WYSIWYG or Markdown.
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.
Step 12: Back Up the Database and Uploads
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.
Step 13: Upgrade BookStack Safely
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.
Optional: Put BookStack Behind a VPN
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.
Troubleshooting
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.
Going Further
- Turn on email. Set the
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. - Add SSO. BookStack supports SAML2, OIDC, and LDAP. If you self-host Authelia or another identity provider, you can put your whole team behind single sign-on and drop per-app passwords.
- Use the API. Every shelf, book, and page is reachable over a token-authenticated REST API - handy for scripting bulk imports, syncing runbooks from a repo, or wiring BookStack into other tools.
- Run it alongside other tools. This same Caddy reverse-proxy pattern powers our guides for Memos and FreshRSS. One small VPS happily hosts all three on separate subdomains.
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.