Start a Self-Hosted Wiki for Documentation

Start a Self-Hosted Wiki for Documentation

I got tired of scattering notes across Notion, Google Docs, and scattered markdown files. When I finally committed to a self-hosted wiki, everything changed. Instead of searching through five different platforms, I now have one unified knowledge base running on my home server—accessible anywhere, fully backed up, and completely under my control.

In this guide, I'll walk you through setting up a production-ready wiki using BookStack (my preference) or MediaWiki, complete with Docker Compose, reverse proxy integration, and security hardening. By the end, you'll have a powerful documentation system that scales with your needs.

Why Self-Host Your Wiki?

I've used Confluence, Notion, and Obsidian. Each had trade-offs. Notion is fast but locks your data behind their API. Obsidian is local-first but doesn't work well for team collaboration. Self-hosting gives me the best of both worlds: full data ownership, collaborative features, powerful search, and zero subscription fees.

BookStack is my top choice because it's lightweight, intuitive, and doesn't require extensive configuration. MediaWiki (the engine behind Wikipedia) is more powerful but steeper to learn. For a homelab? BookStack wins.

You'll need a VPS or home server with at least 2 cores and 2GB RAM. If you're shopping for a budget VPS, RackNerd's KVM plans start at $1.99/month and handle wikis comfortably.

Architecture: Docker Compose + Caddy + PostgreSQL

I'm running this stack:

This setup handles updates gracefully, scales to thousands of pages, and integrates with Tailscale for remote access without exposing ports.

Docker Compose Setup

Create a directory for your wiki and save this as docker-compose.yml:

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: wiki-db
    environment:
      POSTGRES_DB: bookstack
      POSTGRES_USER: bookstack
      POSTGRES_PASSWORD: changeme_secure_password_here
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped
    networks:
      - wiki-network

  bookstack:
    image: linuxserver/bookstack:latest
    container_name: bookstack
    environment:
      - PUID=1000
      - PGID=1000
      - APP_URL=https://wiki.example.com
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_USER=bookstack
      - DB_PASSWORD=changeme_secure_password_here
      - DB_DATABASE=bookstack
      - [email protected]
      - MAIL_HOST=smtp.example.com
      - MAIL_PORT=587
      - [email protected]
      - MAIL_PASSWORD=your-email-password
      - MAIL_ENCRYPTION=tls
    volumes:
      - bookstack_data:/config
    depends_on:
      - postgres
    restart: unless-stopped
    networks:
      - wiki-network

  caddy:
    image: caddy:latest
    container_name: wiki-caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    restart: unless-stopped
    networks:
      - wiki-network

volumes:
  postgres_data:
  bookstack_data:
  caddy_data:
  caddy_config:

networks:
  wiki-network:
    driver: bridge
Watch out: The password changeme_secure_password_here is a placeholder. Generate a strong password with openssl rand -base64 32 before deploying. Never commit real passwords to version control.

Caddy Reverse Proxy Configuration

Create a Caddyfile in the same directory:

wiki.example.com {
  encode gzip
  
  # Security headers
  header / X-Frame-Options "SAMEORIGIN"
  header / X-Content-Type-Options "nosniff"
  header / X-XSS-Protection "1; mode=block"
  header / Referrer-Policy "strict-origin-when-cross-origin"
  header / Permissions-Policy "geolocation=(), microphone=(), camera=()"
  
  # Rate limiting for login
  @login_paths {
    path /login*
    path /auth*
  }
  
  ratelimit @login_paths 5 10s
  
  # Reverse proxy to BookStack
  reverse_proxy bookstack:80 {
    header_uri X-Forwarded-Proto https
    header_uri X-Forwarded-Host {http.request.host}
    header_uri X-Forwarded-For {http.request.remote}
  }
  
  # Caching for static assets
  @static {
    path /public/* /storage/uploads/*
    file
  }
  
  header @static Cache-Control "public, max-age=31536000"
}

Replace wiki.example.com with your actual domain. If you're using a subdomain on a machine behind your firewall, pair this with Tailscale or Cloudflare Tunnel (no port forwarding needed).

Getting Started

Once your compose file and Caddyfile are in place, spin everything up:

docker-compose up -d

Check logs to confirm everything started cleanly:

docker-compose logs -f bookstack

Visit your domain. BookStack will auto-create the database schema on first boot. The default credentials are:

Change these immediately. Go to Settings → Users and update your admin account. Then delete the default user if you want.

Backups and Updates

I back up my wiki using a simple script that runs daily via cron:

#!/bin/bash

BACKUP_DIR="/backups/wiki"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR

# Backup PostgreSQL
docker exec wiki-db pg_dump -U bookstack bookstack | gzip > $BACKUP_DIR/bookstack_db_$DATE.sql.gz

# Backup BookStack files (covers uploaded images, attachments)
docker run --rm -v wiki_bookstack_data:/config -v $BACKUP_DIR:/backup alpine tar czf /backup/bookstack_files_$DATE.tar.gz -C /config .

# Delete backups older than 30 days
find $BACKUP_DIR -name "*.gz" -mtime +30 -delete

echo "Wiki backup completed: $DATE"

Store these backups on a separate drive or upload them to a service like Backblaze B2 or your NAS.

For updates, BookStack releases monthly. Update with one command:

docker-compose pull && docker-compose up -d
Tip: Test updates on a staging instance first. Docker's layered images mean updates are usually quick, but one misconfigured environment variable could lock you out. Having a known-good backup is a lifesaver.

Security Hardening

Out of the box, BookStack is reasonably secure. Here's what I add:

1. Enable LDAP or OAuth if you have it (better than password reuse)

2. Set up fail2ban to block brute-force attempts on the host:

sudo apt install fail2ban

# Create /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5

[sshd]
enabled = true

[caddy-http]
enabled = true
logpath = /var/log/caddy/access.log
port = http,https
filter = caddy-http
maxretry = 10

sudo systemctl restart fail2ban

3. Run backups to an offsite location (I use rclone to sync to Backblaze B2 hourly)

4. Set strong database passwords and rotate them quarterly

5. Use Tailscale for access instead of opening ports (covered separately on CompactHost)

Content Organization Tips

Once your wiki is live, here's how I organize it:

I use BookStack's templates feature heavily. For any repetitive documentation (e.g., server setup checklists, deployment logs), create a template and reuse it. Saves hours.

What's Next?

Your wiki is now running, backed up, and accessible. From here, I'd recommend:

If you don't have a VPS yet and you're running this on shared hardware, consider offloading the database to a managed VPS from RackNerd—their infrastructure is stable and their support is responsive.

```