Many teams outgrow ad-hoc documentation in chat threads and scattered docs once they hit 10–30 engineers, customer support handoffs, and compliance checklists. Outline gives you a clean internal knowledge base with search, collections, and permissions, but most deployment guides stop at “it runs” and skip the operational guardrails you need in production. This guide walks through a production-focused Outline deployment on Ubuntu using Docker Compose, Caddy for TLS/reverse proxy, and PostgreSQL for durable storage, with Redis and MinIO included for app reliability and file handling.
Architecture and flow overview
This stack separates traffic, app runtime, and stateful services so each layer can be monitored, backed up, and upgraded with less risk. Caddy terminates TLS and handles public ingress. Outline serves the app itself. PostgreSQL stores structured data (users, collections, permissions, revisions). Redis handles cache/queue behavior expected by Outline. MinIO provides S3-compatible object storage for attachments so documents remain portable and backups stay predictable. Persistent volumes are isolated per service, and all containers communicate on a dedicated Docker network.
Traffic flow is: browser → Caddy (HTTPS) → Outline container. Internal service flow is: Outline → PostgreSQL + Redis + MinIO. Backup flow is: periodic pg_dump + MinIO bucket sync + encrypted off-host copy.
Prerequisites
- Ubuntu 22.04/24.04 VM with at least 2 vCPU, 4 GB RAM, and 40+ GB disk
- DNS A record (e.g.,
wiki.example.com) pointing to your server - Firewall allowing 22, 80, and 443 from trusted sources
- Docker Engine + Docker Compose plugin installed
- A dedicated Unix account (non-root) with sudo access
- Ability to store encrypted backups off-host (object storage or backup node)
Use a fresh host if possible. Mixing this stack with unrelated containers increases blast radius and troubleshooting complexity.
Step 1: Prepare host and baseline security
Start with package updates, base hardening, and a sane firewall policy. Keep host prep simple but deliberate; the goal is stable operations, not maximum novelty.
sudo apt update && sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg ufw fail2ban
sudo timedatectl set-timezone UTC
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Manual copy fallback: if the button is unavailable in your theme, select inside the code block and copy normally.
Create a working directory with strict permissions. Store all Compose assets there so upgrades and backups are deterministic.
sudo mkdir -p /opt/outline/{caddy,data/postgres,data/redis,data/minio,backups}
sudo chown -R $USER:$USER /opt/outline
chmod -R 750 /opt/outline
cd /opt/outline
Manual copy fallback: copy directly from the terminal-style block above.
Step 2: Create Compose stack for Outline + PostgreSQL + Redis + MinIO + Caddy
The compose file below uses explicit health checks and restart policies so service ordering is less fragile after reboots. Keep versions pinned in production and roll forward intentionally during maintenance windows.
cat > /opt/outline/docker-compose.yml << 'EOF'
services:
postgres:
image: postgres:16
container_name: outline-postgres
restart: unless-stopped
environment:
POSTGRES_DB: outline
POSTGRES_USER: outline
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U outline -d outline"]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
container_name: outline-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- ./data/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
minio:
image: minio/minio:latest
container_name: outline-minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
volumes:
- ./data/minio:/data
outline:
image: outlinewiki/outline:latest
container_name: outline-app
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_started
env_file:
- .env
caddy:
image: caddy:2
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:
default:
name: outline-net
EOF
Manual copy fallback: if copy controls are stripped, manually highlight the YAML and paste into /opt/outline/docker-compose.yml.
Step 3: Configure environment variables and secrets handling
Do not hardcode secrets in compose YAML. Keep them in .env, restrict file permissions, and rotate high-risk keys on a schedule (or immediately after suspected exposure). For production, generate long random values and store the canonical copy in a secrets manager.
cat > /opt/outline/.env << 'EOF'
# Core app
NODE_ENV=production
URL=https://wiki.example.com
PORT=3000
SECRET_KEY=replace_with_64_plus_random_chars
UTILS_SECRET=replace_with_64_plus_random_chars
# Database / cache
DATABASE_URL=postgres://outline:${POSTGRES_PASSWORD}@postgres:5432/outline
REDIS_URL=redis://redis:6379
# Email (example SMTP)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
[email protected]
SMTP_PASSWORD=replace_with_smtp_password
[email protected]
[email protected]
# Object storage via MinIO
AWS_ACCESS_KEY_ID=${MINIO_ROOT_USER}
AWS_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
AWS_REGION=us-east-1
AWS_S3_UPLOAD_BUCKET_NAME=outline
AWS_S3_UPLOAD_BUCKET_URL=https://wiki.example.com/storage
AWS_S3_UPLOAD_MAX_SIZE=26214400
AWS_S3_FORCE_PATH_STYLE=true
AWS_S3_ACL=private
# MinIO bootstrap
MINIO_ROOT_USER=replace_with_minio_admin
MINIO_ROOT_PASSWORD=replace_with_strong_minio_password
POSTGRES_PASSWORD=replace_with_strong_postgres_password
EOF
chmod 600 /opt/outline/.env
Manual copy fallback: you can paste this block into a text editor and save as /opt/outline/.env.
For mature environments, load secrets from Docker secrets or external secret stores and template your env file during deployment. The essential requirement is to avoid secret leakage into shell history, public repos, or support tickets.
Step 4: Configure Caddy reverse proxy and TLS
Caddy simplifies certificate issuance/renewal and gives clean HTTPS defaults. Keep reverse proxy config small and explicit so incidents are easier to reason about.
cat > /opt/outline/caddy/Caddyfile << 'EOF'
wiki.example.com {
encode gzip zstd
@storage path /storage*
handle @storage {
reverse_proxy minio:9000
}
reverse_proxy outline:3000
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
Manual copy fallback: copy and paste the Caddyfile content manually if needed.
If your DNS is behind a proxy/CDN, verify your TLS mode and origin settings first; mismatches here are a common reason for redirect loops and failed login callbacks.
Step 5: Start services and bootstrap MinIO bucket
Bring up the stack, check health, then create the object bucket once. Keep this sequence consistent across environments to reduce one-off drift.
cd /opt/outline
docker compose pull
docker compose up -d
docker compose ps
docker run --rm --network outline-net \
minio/mc sh -c "
mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} &&
mc mb -p local/outline || true
"
Manual copy fallback: execute line-by-line in your shell if one-click copy is unavailable.
After first startup, complete Outline’s web onboarding, create the initial admin user, and connect your authentication provider (email, Google, or other supported SSO) according to your organization’s policy.
Configuration and secret-management best practices
Production quality comes from repeatable controls, not just working containers. Apply these controls from day one:
- Least privilege: use dedicated credentials per service; avoid shared root-like secrets across environments.
- Rotation cadence: rotate SMTP/API/database secrets on a fixed schedule and after staff transitions.
- Encrypted backups: encrypt database and object backups before off-host transfer.
- Change control: pin image tags, test upgrades in staging, and maintain rollback notes.
- Auditability: keep deployment manifests in version control with secrets excluded.
For compliance-sensitive teams, add access logs at the proxy layer, centralize logs, and preserve backup restore drills as auditable evidence.
Verification checklist (post-deploy and after every upgrade)
Run functional and operational checks. A green homepage alone does not prove readiness.
# container health
cd /opt/outline
docker compose ps
# HTTPS + redirect correctness
curl -I http://wiki.example.com
curl -I https://wiki.example.com
# app response
curl -s https://wiki.example.com | head -n 5
# recent logs for errors
docker compose logs --since=15m outline | tail -n 80
docker compose logs --since=15m caddy | tail -n 80
# backup smoke test (example)
pg_dump -h 127.0.0.1 -U outline outline > /opt/outline/backups/outline_$(date +%F).sql
Manual copy fallback: run each command directly in terminal if clipboard scripts are blocked by your CMS.
Additionally verify a real user workflow: login, create document, upload attachment, search it, and confirm revision history. That catches misconfigured storage and permissions earlier than passive monitoring.
Common issues and practical fixes
1) Login callback fails or loops
Usually caused by URL mismatch between Outline and proxy. Confirm URL=https://wiki.example.com exactly matches the public origin and that TLS is terminating cleanly at Caddy.
2) Attachments fail to upload
Check MinIO credentials, bucket existence, and Caddy route for /storage. Verify object storage env vars and path-style setting are consistent.
3) Database connection errors after restart
Validate DATABASE_URL, confirm PostgreSQL health status, and inspect volume permissions. Avoid editing container internals directly; fix configuration in source files.
4) Slow page loads under moderate usage
Inspect VM CPU credits, disk latency, and Redis persistence impact. Scale to dedicated managed PostgreSQL if sustained load grows beyond single-node comfort.
5) TLS certificate issuance problems
Confirm DNS points to the host and port 80 is reachable for ACME challenges. If behind a CDN, ensure origin reachability and SSL mode alignment.
6) Restore worked but users see missing files
This indicates database/object backup mismatch. Always restore database dump and MinIO bucket snapshot from the same checkpoint.
FAQ
Can I run Outline without MinIO?
Yes, but production teams usually need reliable attachment storage and backups. MinIO gives a self-hosted S3-compatible target that keeps architecture portable.
Is Docker Compose enough for production, or should I move to Kubernetes now?
Compose is often enough for small-to-medium internal deployments if you enforce backups, monitoring, and controlled upgrades. Move to Kubernetes when you have clear scaling, multi-tenant, or platform-standardization needs.
How often should I back up this stack?
At minimum: daily full PostgreSQL backup plus frequent object-store syncs. If knowledge churn is high, increase backup frequency and keep periodic immutable snapshots.
What is the safest upgrade approach?
Pin image tags, test in staging, take fresh backups, and upgrade one component set at a time. Always keep a rollback path with previous images and known-good config.
How do I secure admin access for a distributed team?
Use SSO with enforced MFA, restrict direct server access, and centralize audit logs. Keep least-privilege service accounts and avoid sharing credentials in chat.
Can I use managed PostgreSQL while keeping the rest self-hosted?
Absolutely. Many teams keep Outline/Caddy/MinIO on a VM and move PostgreSQL to a managed service for stronger backup and failover characteristics.
How do I validate disaster recovery beyond backups existing?
Run a scheduled restore drill in a separate environment, then verify login, search, attachments, permissions, and document history. A backup is only proven after a successful restore test.
Related Guides
- How to Deploy Authentik with Docker Compose and Traefik
- Deploy BookStack with Docker Compose, Traefik, and MariaDB
- Deploy NocoDB with Docker Compose, Traefik, and PostgreSQL
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.