Self-hosting and Client Delivery
Module 12 · ship · advanced
docker-compose.prod.yml
# Production n8n: Postgres + n8n + Caddy (automatic HTTPS).
# Usage on the server: cp .env.example .env → edit .env → docker compose -f docker-compose.prod.yml up -d
services:
postgres:
image: postgres:17
restart: unless-stopped
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: 10s
timeout: 5s
retries: 10
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
# Database
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
DB_POSTGRESDB_USER: ${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
# Public URL (webhooks, forms, chat, OAuth redirects all use this)
N8N_HOST: ${DOMAIN}
N8N_PROTOCOL: https
N8N_PORT: 5678
WEBHOOK_URL: https://${DOMAIN}/
N8N_PROXY_HOPS: 1
# Encrypts stored credentials. Back it up; without it, credentials are unrecoverable.
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
# Housekeeping
GENERIC_TIMEZONE: ${TZ}
TZ: ${TZ}
N8N_RUNNERS_ENABLED: "true"
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: 336 # hours (14 days)
N8N_DIAGNOSTICS_ENABLED: "false"
volumes:
- n8n_data:/home/node/.n8n
- ./backups:/backups
# Not exposed publicly; Caddy is the only entry point.
expose:
- "5678"
caddy:
image: caddy:2
restart: unless-stopped
depends_on:
- n8n
environment:
DOMAIN: ${DOMAIN}
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
postgres_data:
n8n_data:
caddy_data:
caddy_config: