A production-ready n8n stack running in queue mode with PostgreSQL for persistence and Redis as the job queue. Executions are offloaded from the main server to dedicated worker containers.
┌─────────────┐
Browser │ n8n_server │ (UI + webhook receiver)
──────► │ :5678 │
└──────┬──────┘
│ enqueues jobs
┌──────▼──────┐
│ Redis │ (Bull queue)
└──────┬──────┘
│ dequeues jobs
┌───────────┴───────────┐
│ │
┌─────▼──────┐ ┌──────▼─────┐
│ n8n_worker_1│ │n8n_worker_2│
└─────┬──────┘ └──────┬─────┘
└───────────┬───────────┘
┌──────▼──────┐
│ PostgreSQL │ (workflow & execution data)
└─────────────┘
-
Change the encryption key — replace
N8N_ENCRYPTION_KEYinbase-config.yamlwith a secret of your own:openssl rand -base64 24
Losing this key means losing access to all stored credentials.
-
Change the database credentials — update
POSTGRES_USER,POSTGRES_PASSWORD, and matchingDB_POSTGRESDB_*vars. -
Set your domain — replace
n8n.example.comincompose.ymlwith your actual domain. -
Put n8n behind a reverse proxy — the server only binds to
127.0.0.1:5678. Use nginx or Caddy to terminate TLS and proxy to it.
services:
n8n-base:
image: docker.n8n.io/n8nio/n8n:2.11.0
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "20m"
max-file: "10"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:5678/healthz || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=postgresuser
- DB_POSTGRESDB_PASSWORD=postgrespw
- QUEUE_HEALTH_CHECK_ACTIVE=true
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- N8N_RUNNERS_ENABLED=true
- N8N_RUNNERS_MODE=internal
- N8N_ENCRYPTION_KEY=dzyOcPr84PyVwOsYPcfrCQ2jYz76BNq+
- N8N_LOG_LEVEL=debug
- N8N_LOG_OUTPUT=console
- CODE_ENABLE_STDOUT=true
- N8N_BLOCK_ENV_ACCESS_IN_CODE=true
- N8N_PORT=5678
- N8N_DEFAULT_BINARY_DATA_MODE=database
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=168
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000services:
postgres:
image: postgres:18-alpine
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: postgresuser
POSTGRES_PASSWORD: postgrespw
POSTGRES_DB: n8n
volumes:
- ./postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgresuser -d n8n"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
redis:
image: redis:7-alpine
container_name: redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- ./redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
n8n_server:
extends:
file: base-config.yaml
service: n8n-base
container_name: n8n_server
ports:
- "127.0.0.1:5678:5678"
volumes:
- ./n8n_data:/home/node/.n8n
environment:
- OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
- N8N_EDITOR_BASE_URL=https://n8n.example.com
- WEBHOOK_URL=https://n8n.example.com/
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
- N8N_PROXY_HOPS=1
- N8N_DIAGNOSTICS_ENABLED=false
n8n_worker_1:
extends:
file: base-config.yaml
service: n8n-base
container_name: n8n_worker_1
command: worker
environment:
- NODE_FUNCTION_ALLOW_BUILTIN=*
n8n_worker_2:
extends:
file: base-config.yaml
service: n8n-base
container_name: n8n_worker_2
command: worker
environment:
- NODE_FUNCTION_ALLOW_BUILTIN=*docker compose up -d| Setting | Value | Why |
|---|---|---|
EXECUTIONS_MODE |
queue |
Workers pull jobs from Redis instead of the server executing them |
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS |
true |
Manual test runs also go to workers, keeping the server responsive |
N8N_RUNNERS_MODE |
internal |
Task runners run inside the worker process (no extra containers needed) |
N8N_DEFAULT_BINARY_DATA_MODE |
database |
Binary data stored in Postgres, safe for multi-container setups |
EXECUTIONS_DATA_PRUNE |
true |
Auto-prune executions older than 7 days or over 10k records |
N8N_BLOCK_ENV_ACCESS_IN_CODE |
true |
Prevents workflows from reading host env vars |
Add more workers by duplicating a worker block in compose.yml:
n8n_worker_3:
extends:
file: base-config.yaml
service: n8n-base
container_name: n8n_worker_3
command: worker
environment:
- NODE_FUNCTION_ALLOW_BUILTIN=*Then apply:
docker compose up -d