All articles
TutorialsJul 18, 2026 · 22 min read · By The RDP.sh Team

Self-Host BookStack on a VPS for a Private Wiki

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.com at the server
  • Run linuxserver/bookstack plus MariaDB behind Caddy in one docker-compose.yml
  • Generate an APP_KEY, set APP_URL to 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 80 and 443 open 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.

The APP_KEY is not optional and it must never change once you have real content. BookStack uses it to decrypt stored data; rotate it after users exist and you can lock people out and corrupt encrypted fields. Generate it once, put it in the compose file, and treat it like a password you can't reset.

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_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.
  • The BookStack container serves on port 80 internally. We don't publish it on the host because Caddy reaches it on the Docker network.
  • Pin real version tags rather than latest so an unattended docker compose pull can'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.

On the very first `up`, BookStack can start before MariaDB has finished initializing and log a database connection error. This is harmless - the container retries. If it doesn't settle within a minute, run `sudo docker compose restart bookstack` once the database logs show `ready for connections`.

Step 9: Log In and Lock Down the Admin

Open https://wiki.example.com in a browser. BookStack ships with a default administrator account:

Log in, then immediately:

  1. Click your avatar (top right) → Edit Profile.
  2. Change the email to your real address and set a strong password.
  3. 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
The database dump is the part you cannot rebuild by hand. Test a restore at least once: spin up a throwaway MariaDB container, `zcat` the dump into it, and confirm it loads without errors. A backup you've never restored is a hope, not a 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.