When product, support, and engineering teams scale, knowledge fragments quickly across chats, docs, tickets, and personal notes. Outline gives teams a clean, searchable internal knowledge base, but production deployments need more than a quick container launch. You need durable storage, reliable sessions, secure cookies, TLS, backup strategy, and a predictable upgrade workflow. This guide shows a production-ready deployment of Outline on Ubuntu using Docker Compose, Caddy, PostgreSQL, and Redis, with practical checks and incident-minded operational guidance.
Architecture and flow overview
This stack separates responsibilities so operations remain simple under load: Caddy terminates TLS and reverse-proxies traffic, Outline serves the app, PostgreSQL stores durable content and metadata, and Redis handles cache/session primitives. We isolate services on a private Docker network, expose only the reverse proxy, and persist all stateful services on named volumes. The result is an architecture that is easy to audit, back up, and recover.
- Caddy: HTTPS and reverse proxy.
- Outline: app/API for docs and search.
- PostgreSQL: durable relational state.
- Redis: cache/session queue primitives.
Prerequisites
- Ubuntu 22.04/24.04 server (2 vCPU, 4 GB RAM minimum).
- DNS record (for example
wiki.example.com) pointing to the host. - Sudo access.
- Firewall open on 80/443.
- Docker Engine and Docker Compose plugin installed.
- SMTP credentials for invites and password reset links.
Step 1: Prepare host and baseline security
Patch first, then configure firewall and application paths so operations stay repeatable. A predictable host baseline is what makes incident response fast and low-risk when troubleshooting login outages, certificate issues, or database restores.
sudo apt update && sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg ufw
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo mkdir -p /opt/outline/{caddy,data}
sudo chown -R $USER:$USER /opt/outlineIf the copy button does not work in your browser/editor, manually select the code block and copy it.
Step 2: Create production Docker Compose stack
Keep only Caddy public; all stateful services remain private on the internal network. This boundary lowers attack surface and also prevents accidental direct database exposure.
cat > /opt/outline/docker-compose.yml <<'YAML'
services:
caddy:
image: caddy:2.8
container_name: outline-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on: [outline]
networks: [outline_net]
outline:
image: outlinewiki/outline:latest
container_name: outline-app
restart: unless-stopped
env_file: .env
depends_on: [postgres, redis]
networks: [outline_net]
postgres:
image: postgres:15
container_name: outline-postgres
restart: unless-stopped
environment:
POSTGRES_DB: outline
POSTGRES_USER: outline
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pg_data:/var/lib/postgresql/data
networks: [outline_net]
redis:
image: redis:7-alpine
container_name: outline-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
networks: [outline_net]
networks:
outline_net:
driver: bridge
volumes:
pg_data:
redis_data:
caddy_data:
caddy_config:
YAMLIf the copy button does not work in your browser/editor, manually select the code block and copy it.
Step 3: Configure Caddy for HTTPS
Explicit proxy headers prevent protocol confusion and login/session edge cases. Keep logs enabled from day one so your team can reconstruct events during incidents without rebuilding observability later.
cat > /opt/outline/caddy/Caddyfile <<'CADDY'
wiki.yourdomain.com {
encode zstd gzip
reverse_proxy outline:3000 {
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-For {remote_host}
header_up Host {host}
}
log {
output file /data/access.log
format json
}
}
CADDYIf the copy button does not work in your browser/editor, manually select the code block and copy it.
Step 4: Configuration and secret handling
Generate strong secrets and lock the env file. For mature teams, move to centralized secret management and inject at deploy time. Document ownership for each credential so incident responders know exactly who rotates which secret.
cd /opt/outline
POSTGRES_PASSWORD=$(openssl rand -base64 36 | tr -d '\n')
SECRET_KEY=$(openssl rand -hex 32)
UTILS_SECRET=$(openssl rand -hex 32)
cat > .env <<EOF
URL=https://wiki.yourdomain.com
PORT=3000
NODE_ENV=production
SECRET_KEY=$SECRET_KEY
UTILS_SECRET=$UTILS_SECRET
DATABASE_URL=postgres://outline:$POSTGRES_PASSWORD@postgres:5432/outline
REDIS_URL=redis://redis:6379
PGSSLMODE=disable
FORCE_HTTPS=true
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USERNAME=your_smtp_user
SMTP_PASSWORD=your_smtp_password
[email protected]
EOF
chmod 600 .envIf the copy button does not work in your browser/editor, manually select the code block and copy it.
Step 5: Launch and migrate
Bring up backing services first, then run migrations, then expose full stack. This sequence makes failures easier to isolate and reduces the chance of partially initialized application state.
cd /opt/outline
docker compose pull
docker compose up -d postgres redis
sleep 8
docker compose run --rm outline yarn db:migrate --env=production
docker compose up -d
docker compose psIf the copy button does not work in your browser/editor, manually select the code block and copy it.
Verification
Run these checks before inviting users and declaring go-live. Test from a second browser session to validate first-time login and invite-link flows end to end.
- HTTPS valid and auto-renewing.
- Sign-in and invite email delivery works.
- Create/edit/search latency is acceptable.
- Restart test confirms persistence.
- Backups complete and are restorable.
curl -I https://wiki.yourdomain.com
docker compose ps
docker compose logs --tail=120 outline
docker exec outline-postgres pg_dump -U outline outline | head -n 30If the copy button does not work in your browser/editor, manually select the code block and copy it.
Common issues and fixes
Redirect loop on login
Usually an incorrect URL value or missing X-Forwarded-Proto header.
Emails not sending
Validate SMTP credentials, sender policy, and outbound provider restrictions.
Migrations failing
Wait for PostgreSQL health and verify DATABASE_URL syntax exactly.
Slow response during peaks
Check host CPU steal/memory pressure and scale instance resources.
Upload errors
Increase proxy/body limits and verify reverse proxy timeout behavior.
Unexpected logout/session churn
Confirm Redis availability and stable app secret values across restarts.
Operational hardening notes
Production quality comes from operational discipline, not only deployment success. Add a recurring backup job, monitor certificate renewal, and instrument baseline metrics such as response latency, 5xx rate, and disk consumption for PostgreSQL and Redis. Create a lightweight on-call runbook describing what to check first for login failures, SMTP degradation, and storage pressure. Document exact restore procedures and run practice restores monthly in staging so you can verify both backup integrity and team readiness. Define explicit SLOs for availability and page load latency so teams can distinguish normal variance from true incident behavior.
For security posture, keep the host patched, use SSH keys only, disable password login, and restrict administrative access to trusted IP ranges where possible. If your compliance environment requires stronger controls, front the stack with an identity-aware proxy and central audit logging. As your organization grows, integrate SSO and enforce role boundaries for editors and admins. These practices reduce accidental misconfiguration risk and make audits easier. Also schedule quarterly dependency reviews so container base images, proxy versions, and cryptographic defaults remain current.
Finally, create a predictable release calendar. Bundle low-risk changes weekly and reserve larger version upgrades for approved maintenance windows. Capture pre-change health snapshots, run post-change smoke checks, and maintain rollback criteria that are simple enough to execute under pressure. This process turns upgrades from stressful events into routine operations and prevents documentation platforms from becoming hidden single points of failure.
Internal links and related guides
- https://sysbrix.com/blog/guides-3/deploy-grafana-with-docker-compose-and-traefik-on-ubuntu-production-ready-observability-guide-109
- https://sysbrix.com/blog/guides-3/deploy-nextcloud-with-docker-compose-nginx-and-redis-on-ubuntu-production-guide-105
- https://sysbrix.com/blog/guides-3/n8n-windows-setup-wsl2-advanced-workflows-custom-nodes-database-integration-and-production-ready-automation-100
FAQ
Do I need Kubernetes for Outline?
No. Compose is often enough for small-to-mid teams if backups, monitoring, and upgrades are handled carefully.
What VM size should I start with?
2 vCPU/4 GB RAM is a practical start; move to 4 vCPU/8 GB as users and uploads grow.
Can I use managed PostgreSQL?
Yes, and it can improve durability/ops overhead if network access and TLS are configured correctly.
How often should backups run?
Nightly full backups are baseline; add higher frequency for stricter RPO requirements.
Should Redis be exposed publicly?
No. Keep Redis private on the Docker network and avoid public bind ports.
How do I upgrade safely?
Stage first, snapshot data, run migrations, verify critical flows, then promote to production.
What if JavaScript is stripped by the editor?
The manual-copy fallback text under each code block ensures users can still copy commands reliably.
Talk to us
Need help deploying a production-ready Outline knowledge platform, integrating SSO, or building secure backup and upgrade runbooks for your team? We can help with architecture, hardening, migration, and operational readiness.