I run my whole agency on BookStack, including the BookStack instance this post originally lived on, which feels like a recursion joke I have to flag upfront. The platform powers internal runbooks, client-facing handover documentation, and the staging area for every blog post on this site. After three years of using it as the single source of truth for agency knowledge, the BookStack self-hosted documentation deployment has settled into a pattern I’m comfortable recommending.
This post is the actual stack I ship: the LinuxServer.io BookStack image, a MariaDB container, and Nginx Proxy Manager in front for TLS. About 30 minutes from a fresh server to a working wiki on a custom domain.
If you’ve never run BookStack before, copy the compose file below, change the placeholder values, and read the backup section before you put a single page of real documentation in. The backup discipline is where most self-hosted wikis quietly fail.
Why BookStack beats Notion for agency documentation
I tried to make Notion work for client handover docs for about 18 months. The pitch is seductive: friendly UI, real-time collaboration, sharing links that “just work.” The reality, once you’ve handed over more than a handful of clients, looks different.
Notion exports are a known problem. You get a zip of markdown files where every internal link is broken, every embedded database loses its formulas, and every nested page becomes its own folder. Hand a client that archive at the end of an engagement and you’re giving them a museum exhibit, not a working knowledge base. I had two clients in 2023 ask me, six months after handover, “where can I see the docs you wrote for us?” and the honest answer was “you’d have to log back into a Notion workspace I don’t pay for anymore.”
BookStack flips that. Export is HTML, PDF, plain text, or markdown, in two UI clicks. Link structure survives the HTML export. The markdown is clean enough to drop into any other wiki. When an engagement ends, the client gets a working archive that opens in a browser without an account. Per-seat pricing also doesn’t apply: BookStack runs on a 4€/month VPS regardless of how many accounts you hand out.
The BookStack deployment stack
Here’s what runs on the box. Two containers (BookStack and MariaDB) plus Nginx Proxy Manager for TLS.
Prerequisites
A VPS with at least 2GB of RAM. A 1GB box runs BookStack for a single-user knowledge base, but MariaDB is happier with breathing room once you cross a few hundred pages with attached images. For agency-scale use, 2GB or 4GB is the right starting point.
You also need a hardened server before anything else lands on it. SSH keys, no root login, UFW with deny-by-default. If you haven’t done that yet, my Linux server security fundamentals post is the baseline. A wiki containing client credentials is a high-value target; treat the server like one.
DNS access for the domain. Cloudflare is the pragmatic default if you want API-driven Let’s Encrypt challenges and WAF rules in front.
Decide upfront whether the wiki is internal-only or client-facing. For internal runbooks I keep BookStack behind a Wireguard tunnel and skip the public DNS step. For client-facing docs I expose it on a public subdomain with TLS and per-client accounts.
Docker engine
Install Docker from the official Docker repository. Follow the official Docker installation guide for Ubuntu. Don’t install from the default apt repo or from a one-line curl | sh script. The official repository is the only correct source.
Nginx Proxy Manager
Nginx Proxy Manager (NPM) handles TLS termination, Let’s Encrypt renewal, and HTTP-to-HTTPS redirects through a web UI. If you’ve already set it up alongside Vaultwarden following my Portainer + NPM + Vaultwarden post, skip ahead.
BookStack and MariaDB
The BookStack stack is the LinuxServer.io image plus a MariaDB container they also maintain. Single compose file, two services, one database volume, one application volume.
version: "2"
services:
bookstack:
image: lscr.io/linuxserver/bookstack
container_name: bookstack
environment:
- PUID=1000
- PGID=1000
- APP_URL=https://docs.yourdomain.com
- DB_HOST=bookstack_db
- DB_USER=bookstackuser
- DB_PASS=ReplaceWithStrongPassword
- DB_DATABASE=bookstackapp
- SESSION_SECURE_COOKIE=true
- SESSION_COOKIE_NAME=bookstack_session
- MAIL_DRIVER=smtp
- MAIL_FROM=docs@yourdomain.com
- MAIL_FROM_NAME="Agency Docs"
- MAIL_HOST=smtp.yourprovider.com
- MAIL_PORT=587
- MAIL_USERNAME=smtp-username
- MAIL_PASSWORD=smtp-password
- MAIL_ENCRYPTION=tls
- ALLOWED_IFRAME_HOSTS="*"
- ALLOWED_IFRAME_SOURCES="*"
# - APP_DEFAULT_DARK_MODE=true
volumes:
- ./bookstack/data:/config
ports:
- 6885:80
restart: unless-stopped
depends_on:
- bookstack_db
bookstack_db:
image: lscr.io/linuxserver/mariadb:latest
container_name: bookstack_db
environment:
- PUID=1000
- PGID=1000
- MYSQL_ROOT_PASSWORD=ReplaceWithStrongRootPassword
- TZ=Europe/Bratislava
- MYSQL_DATABASE=bookstackapp
- MYSQL_USER=bookstackuser
- MYSQL_PASSWORD=ReplaceWithStrongPassword
volumes:
- ./bookstack/db:/config
restart: unless-stopped
The values that absolutely must change before you bring this up: every password, the APP_URL, the SMTP block (or set MAIL_DRIVER=log if you don’t want outbound mail yet), and TZ to match your region.
A few things worth calling out:
DB_PASSandMYSQL_PASSWORDmust match. Mismatch them and BookStack hangs at the DB connection check and fails to boot.APP_URLis the public URL the wiki will live on. Get this right before first boot. BookStack writes the URL into generated content (image references, password reset links), and changing it later means a database fix.ALLOWED_IFRAME_HOSTSandALLOWED_IFRAME_SOURCESare wide-open. Tighten them once you know which sources you actually embed. For client-facing wikis with no embeds, set both to an empty string.- Uncomment
APP_DEFAULT_DARK_MODE=trueif you want dark mode as the default.
Bring it up:
docker compose -f /path/to/bookstack.yml up -d
In NPM, add a Proxy Host pointing docs.yourdomain.com to http://bookstack:6885, request a Let’s Encrypt certificate, tick “Force SSL” and “HTTP/2 Support”. On the Advanced tab, add the standard security headers (HSTS, X-Frame-Options, a Content-Security-Policy that allows the iframe sources you actually need).
First-login setup
Browse to the URL. The default credentials are admin@admin.com with the password password. Change them before you do anything else. Settings, Users, edit the admin, set a strong unique password and an email you actually own. The first time I deployed BookStack I left the default for “just five minutes” while I went to make coffee, and got a polite reminder from a friend running an internet-scanner project that those credentials are the first guess on every wiki scanner.
After that, a few settings worth flipping:
- Settings, General, Site name. The name that appears in the header and emails.
- Settings, Roles. The “public” role is on by default. Turn it off if the wiki is internal-only.
- Settings, Auth. If you’re running Authentik, wire BookStack’s SAML or OIDC integration here. Local accounts are fine for a single user.
- Settings, Customization, Logo. Replace the default, especially if it’s client-facing.
The backup pattern that actually works
BookStack backups are two artifacts, taken together.
The first is a daily MariaDB dump. The full text of every page lives in the database, so the dump is the canonical source of truth. I run mysqldump from a cron on the host, ship the resulting .sql.gz to a remote restic repository on Backblaze B2, and keep a 30-day rolling window. A dump from a 1,000-page wiki takes a few seconds and lands at maybe 50 megabytes.
docker exec bookstack_db sh -c \
'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" bookstackapp' \
| gzip > /backups/bookstack-$(date +%F).sql.gz
The second is the BookStack data volume. The ./bookstack/data directory holds uploaded images and attachments. The database references these files but doesn’t contain them. Lose the volume and every embedded screenshot is broken. Snapshot the volume with restic or your VPS provider’s native backup, on the same daily schedule.
Test the restore once, before you have a real incident. The first time I tried to restore a BookStack instance, I discovered I’d been backing up the database but not the uploads volume. The pages came back. Every screenshot was a broken-image icon. That was the day I started running monthly restore drills on the boxes that matter.
How I structure an agency BookStack instance
The deployment is easy; the information architecture is what makes a wiki useful or makes it a graveyard. The structure I’ve landed on:
- One shelf per business function. Internal Operations, Client Engagements, Service Catalogue, Vendors, Onboarding.
- One book per client, one book per recurring service. Every client gets a book in Client Engagements; every service gets a book in Service Catalogue with its runbook.
- Chapters as life-cycle stages. For client books: Discovery, Implementation, Run, Handover.
- Pages as the smallest reusable unit. “If I’d want to link to it as a single thing in Slack, it’s a page.”
Permissions map onto this cleanly. Internal team gets access to everything; per-client guest accounts see only their book. The hierarchy makes “share this chapter with the client” a real operation rather than a forward-and-pray copy-paste.
If you also run Plausible analytics on the wiki domain, you’ll see which pages clients actually open after handover, which is more useful for shaping documentation than any internal review.
Sizing and what I don’t bother with
Resource usage is modest. My production wiki runs about 12% CPU and 600MB of RAM with roughly 40 books, 800 pages, and 200MB of embedded media. A 2GB VPS is comfortable. MariaDB is the larger of the two containers; BookStack itself is a Laravel app.
A few things I’ve left out of this stack:
- PostgreSQL backend. Supported, but the LinuxServer MariaDB image works and the operational tools (
mysqldump, migrations) are well-trodden. - External S3-compatible storage. Useful at scale, unnecessary at agency scale.
- Self-hosted full-text search engines. BookStack’s built-in search is fine for under 5,000 pages.
- Custom themes. The default is good. Spend the time on writing actual documentation.
The one upgrade I do recommend for client-facing wikis is pairing BookStack with Uptime Kuma and treating the wiki URL as a monitored production service. Documentation is worth nothing during the incident when you can’t reach it.
The meta-migration aside
Quick housekeeping note: this post originally lived on a BookStack instance, the same instance running BookStack the way I’m describing here. I’m migrating the agency’s archive of deployment guides off that BookStack and into the public Insights archive on Astro, which is why it exists in two places during the transition. Every post on the BookStack side came out as clean markdown that needed maybe 15 minutes of cleanup before it was publishable. Try doing that with three years of Notion history.
Verifying the deployment
Run this sanity check before you put real documentation in:
- Create a test book, chapter, and page. Verify the search index picks it up after a couple of seconds.
- Upload an image, then run
docker compose down && docker compose up -d. Verify the image still renders. - Log out, log back in. Verify the session cookie behaves with
SESSION_SECURE_COOKIE=trueover HTTPS. (Misconfigured proxy gives you a redirect loop here.) - Take a
mysqldump. Restore it to a separate MariaDB container on a different host. Bring up a second BookStack pointing at the restored database. Verify the test content shows up.
Step 4 is the one people skip. Do it once, on day one, before you have anything important inside.
Closing the loop
This BookStack stack has been the agency’s knowledge base for three years. It has survived two server migrations, one accidental drop of the wrong volume (recovered from the daily dump), and roughly a 30-fold increase in page count. Setup time is a couple of hours including security hardening, recurring cost is a 4€/month VPS, and the operational burden is one daily backup job and the occasional MariaDB minor-version bump.
What self-hosting a wiki has actually given me, beyond data ownership and export portability, is a place where institutional knowledge accumulates instead of evaporating. When the agency’s third hire onboarded last year, “where do I find X” had an answer in 90% of cases without needing to interrupt anyone. The companion read for that side of it is my Vikunja task management post, since a wiki without a task system next to it is half a knowledge layer. The rest of the open source solutions archive covers the other pieces.