Skip to Content

Production Guide: Deploy Outline with Docker Compose + Nginx + PostgreSQL on Ubuntu

A production-focused Outline deployment with secure networking, secret handling, backups, verification, and incident-ready troubleshooting.

Many teams outgrow ad-hoc documentation the moment incidents start repeating: a failed release because a migration step lived only in chat history, an on-call handoff without a runbook, or customer-facing procedures scattered across private notes. Outline is a practical documentation platform when you need searchable team knowledge, clean editing, granular permissions, and a deployment pattern that operations teams can maintain without guesswork. In this production guide, you will deploy Outline on Ubuntu using Docker Compose, Nginx as the reverse proxy, and PostgreSQL as the database, with Redis for cache/session workloads that Outline expects in real deployments.

The goal is not just “it runs.” The goal is a system you can operate under pressure: secrets handled safely, predictable restart behavior, health checks, TLS termination, backups, restore testing, and troubleshooting paths that match common field failures. We also include validation commands and practical hardening choices so you can prove the stack is healthy before inviting editors and stakeholders.

This approach is especially useful for engineering organizations managing internal architecture docs, SRE runbooks, product SOPs, and compliance evidence repositories. By separating web ingress, app runtime, cache, and database responsibilities, you reduce blast radius and keep upgrades safer. You can roll app versions independently while preserving stateful data, and you can rehearse disaster recovery with deterministic backup artifacts instead of assumptions.

Architecture and flow overview

The deployment has four services: outline (application), postgres (primary data store), redis (cache/queue/session support), and nginx (edge reverse proxy). Nginx terminates TLS and forwards requests to Outline over an internal Docker network. PostgreSQL and Redis are not exposed to the public internet; only Nginx binds host ports 80/443. This design preserves least-privilege network boundaries and simplifies security controls.

Request flow: browser → HTTPS on Nginx → upstream to Outline container → Outline reads/writes PostgreSQL and Redis over private network. Persistent data is held in named volumes for database durability and local app storage. For backup and recovery, you capture PostgreSQL dumps plus app storage artifacts, then verify restores in a staging environment. This gives you measurable recovery confidence instead of “backup completed” logs with unknown restore quality.

Operationally, keep config under a dedicated directory such as /opt/outline owned by a service account. Treat that path as your deployment boundary: compose file, environment file, helper scripts, and backups live together. This makes permissions, auditing, and automation easier during upgrades and incidents.

Prerequisites

  • Ubuntu 22.04 or 24.04 host with Docker Engine and Docker Compose plugin
  • Public DNS record for your docs hostname (example: docs.example.com)
  • TLS certificate strategy (Let’s Encrypt via certbot or pre-provisioned certs)
  • Minimum 2 vCPU / 4 GB RAM for small teams; increase for larger usage
  • Shell access with sudo and outbound network access for image pulls

Before deployment, reserve a maintenance window and define ownership: who handles backups, certificate renewals, patching cadence, and rollback decisions. Documentation platforms fail operationally when ownership is ambiguous.

Step-by-step deployment

1) Create service account and directory layout

sudo useradd --system --create-home --home-dir /opt/outline --shell /usr/sbin/nologin outline
sudo mkdir -p /opt/outline/{nginx,storage,backups}
sudo chown -R outline:outline /opt/outline

If the copy button does not work in your browser/editor, manually select the command block and copy.

This keeps runtime assets isolated and avoids root-owned files from ad-hoc commands. The dedicated account also makes permission audits straightforward.

2) Create environment file with strong secrets

sudo -u outline bash -lc 'cd /opt/outline && cat > .env <<EOF
DOMAIN=docs.example.com
APP_PROTOCOL=https
APP_PORT=3000
SECRET_KEY=CHANGE_ME_64PLUS_CHARS
UTILS_SECRET=CHANGE_ME_64PLUS_CHARS
POSTGRES_USER=outline
POSTGRES_PASSWORD=CHANGE_ME_DB_PASSWORD
POSTGRES_DB=outline
REDIS_URL=redis://redis:6379
DATABASE_URL=postgres://outline:CHANGE_ME_DB_PASSWORD@postgres:5432/outline
URL=https://docs.example.com
FORCE_HTTPS=true
EOF
chmod 600 .env'

If the copy button does not work in your browser/editor, manually select the command block and copy.

Generate strong random secrets and keep .env out of Git. If this host is shared, consider Docker secrets or external secret managers for stricter controls.

3) Define Docker Compose services

sudo -u outline bash -lc 'cd /opt/outline && cat > docker-compose.yml <<EOF
services:
  postgres:
    image: postgres:16
    container_name: outline-postgres
    restart: unless-stopped
    env_file: .env
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 20s
      timeout: 5s
      retries: 10
    networks: [outline_internal]

  redis:
    image: redis:7-alpine
    container_name: outline-redis
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    networks: [outline_internal]

  outline:
    image: outlinewiki/outline:latest
    container_name: outline-app
    restart: unless-stopped
    env_file: .env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - ./storage:/var/lib/outline/data
    expose:
      - "3000"
    networks: [outline_internal]

  nginx:
    image: nginx:1.27-alpine
    container_name: outline-nginx
    restart: unless-stopped
    depends_on:
      - outline
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    networks: [outline_internal]

networks:
  outline_internal:
    driver: bridge

volumes:
  postgres_data:
  redis_data:
EOF'

If the copy button does not work in your browser/editor, manually select the command block and copy.

Keep only Nginx externally reachable. Postgres/Redis should remain internal services with no host port bindings.

4) Add Nginx reverse-proxy config

sudo -u outline bash -lc 'cd /opt/outline && cat > nginx/default.conf <<EOF
server {
  listen 80;
  server_name docs.example.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  server_name docs.example.com;

  ssl_certificate     /etc/nginx/certs/fullchain.pem;
  ssl_certificate_key /etc/nginx/certs/privkey.pem;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:10m;
  ssl_protocols TLSv1.2 TLSv1.3;

  client_max_body_size 25m;

  location / {
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_pass http://outline:3000;
    proxy_read_timeout 120s;
  }
}
EOF'

If the copy button does not work in your browser/editor, manually select the command block and copy.

If your certificates are provisioned by certbot on host paths, map those directories into the Nginx container read-only and update paths accordingly.

5) Start stack and run first health checks

sudo -u outline bash -lc 'cd /opt/outline && docker compose pull'
sudo -u outline bash -lc 'cd /opt/outline && docker compose up -d'
sudo -u outline bash -lc 'cd /opt/outline && docker compose ps'

If the copy button does not work in your browser/editor, manually select the command block and copy.

After startup, wait for PostgreSQL health to pass and confirm Outline is stable (no restart loop). Early failures usually trace to bad secrets, malformed URLs, or certificate mount paths.

6) Configure automated backup and retention

sudo -u outline bash -lc 'cd /opt/outline && cat > backups/backup_outline.sh <<EOF
#!/usr/bin/env bash
set -euo pipefail
cd /opt/outline
source .env
STAMP=$(date +%F-%H%M%S)
mkdir -p backups/$STAMP

docker compose exec -T postgres sh -lc "pg_dump -U $POSTGRES_USER -d $POSTGRES_DB" > backups/$STAMP/outline.sql
cp -a storage backups/$STAMP/storage

tar -czf backups/outline-backup-$STAMP.tar.gz -C backups $STAMP
rm -rf backups/$STAMP
EOF
chmod +x backups/backup_outline.sh'

If the copy button does not work in your browser/editor, manually select the command block and copy.

Schedule this script daily and prune old archives to policy. If compliance requires immutable retention, replicate archives to object storage with versioning and write-once controls.

Configuration and secrets handling best practices

Use high-entropy secrets and rotate deliberately: treat SECRET_KEY, UTILS_SECRET, and database credentials as first-class operational secrets. Rotate during controlled windows and validate login, document editing, and search functionality after each rotation. Keep rotation runbooks in the same platform so responders can execute repeatably.

Constrain trust boundaries: expose only Nginx, keep app/database/cache on private network, and avoid direct database host ports. Enforce host firewall rules (UFW or cloud SGs) so only 80/443 are reachable publicly. If remote administration is needed, restrict SSH source ranges and require key-based auth.

Harden HTTP behavior: apply strict TLS policy, reasonable body limits, and explicit proxy headers. Add security headers and rate limits if your environment requires stronger internet-facing posture. Monitor certificate expiration and alert before renewal windows close.

Treat backups as a product: a backup that cannot be restored is not a backup. Run regular restore drills in isolated staging, verify a sampled set of pages/attachments, and record measured RTO/RPO. This operational evidence is critical for security reviews and business continuity planning.

Verification checklist

Run these checks before opening access to broader teams:

# service health
sudo -u outline bash -lc 'cd /opt/outline && docker compose ps'

# HTTP response and TLS path
curl -I https://docs.example.com

# inspect recent logs for errors
sudo -u outline bash -lc 'cd /opt/outline && docker compose logs --since=10m outline postgres redis nginx'

# run one backup and verify archive exists
sudo -u outline bash -lc '/opt/outline/backups/backup_outline.sh && ls -lh /opt/outline/backups | tail -n 5'

If the copy button does not work in your browser/editor, manually select the command block and copy.

Then execute a quarterly restore drill: restore SQL and storage into a staging host, confirm authentication works, verify a recent page with attachments, and document timing plus deviations from target objectives.

Common issues and fixes

502 Bad Gateway from Nginx

Most 502 errors come from Outline not listening yet, wrong upstream target, or failing app startup. Check docker compose ps, inspect Outline logs, and confirm Nginx config points to outline:3000 inside the same Docker network.

Database connection failures during boot

Verify DATABASE_URL format and credentials, ensure PostgreSQL healthcheck is passing, and confirm no accidental special-character escaping errors in .env. Restart app after fixing secrets to reload runtime environment.

Login loop or incorrect callback URLs

Mismatch between public URL and app URL variables is common. Ensure URL and proxy headers indicate HTTPS and correct hostname. Clear browser cookies after URL corrections and retest.

Backups created but restore fails

Typical causes are incomplete dumps, permission drift in storage files, or missing dependency versions on restore target. Validate dump non-empty, keep restore script versioned, and test monthly to catch drift early.

FAQ

Can I deploy Outline without Redis?

For production use, Redis is strongly recommended for expected performance and background behavior. Skipping it may work temporarily but increases risk of unstable behavior under load.

Should I use managed PostgreSQL instead of a local container?

If your organization already runs managed databases with backups/HA, that is often preferable. Keep network ACLs tight and update DATABASE_URL accordingly.

How often should I update Outline images?

Adopt a regular cadence (for example monthly) plus emergency patch windows for security advisories. Always test upgrades in staging before production rollout.

What is a safe minimum backup policy?

Daily backup, retention aligned to business policy, off-host replication, and scheduled restore drills. Monitor backup job success/failure as alertable events.

How do I reduce downtime during maintenance?

Use maintenance windows, pre-pull images, run prechecks, and keep rollback image tags documented. Fast rollback plans matter more than perfect first attempts.

Can this setup scale for larger teams?

Yes. Scale by moving PostgreSQL to managed HA, adding stronger monitoring, and introducing load-balancing patterns around the app tier as usage grows.

Related internal guides

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

Production Guide: Deploy Netdata with Docker Compose + Caddy on Ubuntu
A production-focused deployment pattern for Netdata with secure proxying, sane retention, alerting, and operational runbooks.