Outline is a polished, open-source knowledge base for teams that need internal documentation without handing every runbook, onboarding note, and incident review to a hosted SaaS platform. A practical deployment has more moving parts than a quick demo: the application needs stable PostgreSQL storage, Redis for queues and sessions, HTTPS termination, email delivery, persistent uploads, backups, and a repeatable upgrade path.
This guide follows the current SysBrix Guides pattern for production-focused self-hosting. We will deploy Outline on Ubuntu with Docker Compose, place Caddy in front for automatic TLS, keep PostgreSQL and Redis on a private Docker network, and add operational checks so the service can be maintained after the first successful login.
Architecture and flow overview
The public request path is intentionally simple. Users connect to https://docs.example.com, Caddy terminates TLS, and Caddy forwards traffic to the Outline container on the private Compose network. Outline stores structured application data in PostgreSQL, uses Redis for short-lived coordination, and writes uploaded files to a mounted host directory. Nothing except Caddy binds to the public internet.
The operational flow is also straightforward: configuration lives in /opt/outline/.env, Compose defines the services, Caddy owns certificates, and a systemd timer exports database and upload backups each night. For larger installations you can replace local file storage with S3-compatible object storage later, but starting with local storage keeps recovery easy and visible.
Prerequisites
- Ubuntu 22.04 or 24.04 server with at least 2 vCPU, 4 GB RAM, and 40 GB disk.
- A DNS record such as
docs.example.compointing to the server. - Outbound access for ACME certificates and SMTP.
- An SMTP account or relay, because Outline relies on email for invitations and sign-in flows.
- Root or sudo access and a basic backup destination outside the server.
Step-by-step deployment
Start by installing Docker, Compose, baseline packages, and a minimal firewall. Keep SSH open and expose only ports 80 and 443 for Caddy. If your server already has Docker installed, still confirm that the Compose plugin is available with docker compose version.
sudo apt update && sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg ufw openssl
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
If the copy button is unavailable in your browser, select the block and copy it manually.
Create a dedicated application directory and generate secrets before writing the environment file. The two Outline secrets must be long, random, and stable; changing them later can invalidate sessions and signed URLs. Keep them outside chat tools and ticket descriptions.
sudo mkdir -p /opt/outline/{data,postgres,redis,caddy,backups}
sudo chown -R $USER:$USER /opt/outline
cd /opt/outline
openssl rand -hex 32 > .secret_key
openssl rand -hex 32 > .utils_secret
chmod 600 .secret_key .utils_secret
If the copy button is unavailable in your browser, select the block and copy it manually.
Now create the environment file. Replace every placeholder before starting the stack. Use the exact values from .secret_key and .utils_secret, set the public domain, and use an app-specific SMTP password rather than a personal mailbox password.
cat > /opt/outline/.env <<'EOF'
DOMAIN=docs.example.com
[email protected]
POSTGRES_PASSWORD=replace-with-long-random-postgres-password
SECRET_KEY=replace-with-output-of-cat-secret_key
UTILS_SECRET=replace-with-output-of-cat-utils_secret
SMTP_HOST=smtp.example.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=replace-with-smtp-app-password
[email protected]
EOF
chmod 600 /opt/outline/.env
If the copy button is unavailable in your browser, select the block and copy it manually.
The Compose file keeps all application services on an internal network and publishes only Caddy. PostgreSQL uses a health check so Outline does not race the database on boot, Redis is private to the stack, and the Outline upload directory is persisted on the host.
cat > /opt/outline/docker-compose.yml <<'EOF'
services:
outline:
image: docker.getoutline.com/outlinewiki/outline:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
env_file: .env
environment:
URL: https://${DOMAIN}
PORT: 3000
DATABASE_URL: postgres://outline:${POSTGRES_PASSWORD}@postgres:5432/outline
REDIS_URL: redis://redis:6379
SECRET_KEY: ${SECRET_KEY}
UTILS_SECRET: ${UTILS_SECRET}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_USERNAME: ${SMTP_USERNAME}
SMTP_PASSWORD: ${SMTP_PASSWORD}
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL}
SMTP_REPLY_EMAIL: ${SMTP_FROM_EMAIL}
FORCE_HTTPS: "true"
ENABLE_UPDATES: "false"
FILE_STORAGE: local
FILE_STORAGE_LOCAL_ROOT_DIR: /var/lib/outline/data
volumes:
- ./data:/var/lib/outline/data
networks: [internal]
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: outline
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: outline
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U outline -d outline"]
interval: 10s
timeout: 5s
retries: 10
networks: [internal]
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--save", "", "--appendonly", "no"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
networks: [internal]
caddy:
image: caddy:2-alpine
restart: unless-stopped
depends_on: [outline]
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy:/data
networks: [internal]
networks:
internal:
driver: bridge
EOF
If the copy button is unavailable in your browser, select the block and copy it manually.
Caddy handles certificates automatically when DNS is correct. The security headers below are conservative defaults for a documentation site, and the reverse proxy target stays inside the Docker network.
cat > /opt/outline/Caddyfile <<'EOF'
{$DOMAIN} {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
}
reverse_proxy outline:3000
}
EOF
docker compose --env-file .env up -d
sleep 20
docker compose ps
If the copy button is unavailable in your browser, select the block and copy it manually.
Run migrations after the containers are healthy. If the health endpoint is unavailable during the first minute, inspect logs rather than repeatedly recreating containers; most first-run failures are environment or SMTP mistakes.
cd /opt/outline
docker compose exec outline yarn db:migrate --env=production-ssl-disabled
docker compose logs --tail=80 outline
curl -I https://docs.example.com
If the copy button is unavailable in your browser, select the block and copy it manually.
Configuration and secrets handling best practices
Treat .env, generated secrets, SMTP credentials, and database backups as sensitive material. Restrict permissions to the deployment user, avoid committing them to Git, and store an encrypted copy in your password manager or secrets vault. If multiple admins need access, share through the vault rather than pasting values into messages.
For production teams, set a written rotation process. The PostgreSQL password can be rotated with a maintenance window and an updated environment file. The Outline application secrets should not be rotated casually because they protect signed state. If they must change after a suspected leak, schedule user re-authentication and verify upload links afterward.
Local file storage is acceptable for small teams when backups are tested. For higher durability, move attachments to an S3-compatible bucket with lifecycle rules and server-side encryption, then adjust Outline storage variables. Keep the database backup and file backup from the same timestamp so restores do not produce missing attachments.
Backups and upgrade routine
The backup script exports PostgreSQL and the upload directory into a timestamped archive. This is the minimum useful recovery unit. Copy the resulting archive off-server with your normal backup agent, object storage sync, or encrypted restic repository.
cat > /opt/outline/backup-outline.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
cd /opt/outline
stamp=$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p backups/$stamp
docker compose exec -T postgres pg_dump -U outline -d outline > backups/$stamp/outline.sql
rsync -a data/ backups/$stamp/data/
tar -C backups -czf backups/outline-$stamp.tar.gz $stamp
rm -rf backups/$stamp
find backups -name 'outline-*.tar.gz' -mtime +14 -delete
EOF
chmod 700 /opt/outline/backup-outline.sh
sudo tee /etc/systemd/system/outline-backup.service >/dev/null <<'EOF'
[Unit]
Description=Backup Outline knowledge base
[Service]
Type=oneshot
ExecStart=/opt/outline/backup-outline.sh
EOF
sudo tee /etc/systemd/system/outline-backup.timer >/dev/null <<'EOF'
[Unit]
Description=Run Outline backup nightly
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now outline-backup.timer
If the copy button is unavailable in your browser, select the block and copy it manually.
Before upgrades, run a manual backup, read Outline release notes, and upgrade during a quiet window. The safe routine is docker compose pull, docker compose up -d, migration check, login test, and then a short restore drill in staging at least once per quarter.
Verification checklist
Validate the deployment from the host and from a separate browser. You want to prove that containers are healthy, TLS works externally, email invites can be sent, backups are scheduled, and the application can create pages and upload files.
cd /opt/outline
docker compose ps
docker compose exec postgres pg_isready -U outline -d outline
docker compose exec redis redis-cli ping
curl -fsS https://docs.example.com/api/health | jq . || curl -I https://docs.example.com
sudo systemctl list-timers outline-backup.timer
ls -lh /opt/outline/backups | tail
If the copy button is unavailable in your browser, select the block and copy it manually.
- Create the first workspace and invite a test user through SMTP.
- Create a document, upload a small file, and confirm it remains after
docker compose restart outline. - Run
/opt/outline/backup-outline.shonce manually and confirm the archive exists. - Check Caddy logs after DNS propagation to confirm certificate issuance succeeded.
Common issues and fixes
Outline starts but login emails never arrive
Check SMTP host, port, username, password, and allowed sender. Many providers require app passwords or verified sender identities. Review docker compose logs outline for authentication and TLS errors.
Caddy cannot issue a certificate
Confirm the DNS record points to the server and that ports 80 and 443 are reachable from the internet. Temporarily stop any other web server using those ports and review docker compose logs caddy.
Uploads disappear after a restart
Verify the ./data:/var/lib/outline/data volume is present and writable by the container. If you move to object storage, migrate and test attachments before deleting local files.
Database migrations fail
Stop the app, take a database dump if possible, then rerun migrations with logs open. Do not delete the PostgreSQL volume to “fix” migrations unless you intentionally want a blank instance.
The site works internally but not from the browser
Check firewall rules, cloud security groups, DNS, and the Caddy container port mapping. Also confirm URL in the environment file matches the public HTTPS URL exactly.
FAQ
Can I run Outline without SMTP?
Not realistically for a team deployment. SMTP is part of invitations and sign-in workflows, so configure a reliable relay before onboarding users.
Should PostgreSQL be outside Docker?
For many small teams, the Compose-managed PostgreSQL service is acceptable if backups are tested. Larger organizations may prefer managed PostgreSQL for monitoring, patching, and point-in-time recovery.
Can I use NGINX instead of Caddy?
Yes, but Caddy reduces certificate maintenance and matches the lightweight pattern used in several SysBrix guides. If your platform standard is NGINX, keep the same private network and proxy only the Outline service.
How often should I back up Outline?
Nightly backups are a good starting point. Increase frequency if documentation changes are business-critical, and test restores after meaningful schema or storage changes.
Is local file storage production ready?
It can be for smaller teams when the server disk is reliable and backups are copied off-host. Object storage is better when attachments are large, retention requirements are strict, or multiple environments need shared durability.
How do I monitor the stack?
Monitor HTTPS availability, container restarts, disk usage, PostgreSQL backup age, and SMTP failures. Add log shipping if Outline becomes a dependency for incident response or onboarding.
What is the safest upgrade process?
Back up first, pull images, recreate containers, run or verify migrations, test login and document creation, then keep the previous backup until the new version has been stable for several days.
Related internal guides
- Joplin Server with Docker Compose, Caddy, and PostgreSQL
- HedgeDoc with Docker Compose, Caddy, and PostgreSQL
- Paperless-ngx with Docker Compose, Caddy, PostgreSQL, and Redis
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.