Self-hosted S3-compatible object storage for backups: MinIO behind an Nginx proxy, with an rclone-based backup agent and versioned buckets. Your offsite backups without a cloud bill.
Self-hosted S3-compatible object storage for backups: MinIO behind an Nginx proxy, with an rclone-based backup agent and versioned buckets. Your offsite backups without a cloud bill.
n8n in queue mode with Redis, PostgreSQL persistence, and proper process supervision — the boring, reliable setup that survives restarts and workload spikes. The automation layer behind several of my own integrations.
A deployment guide with working examples — not a downloadable product. Everything below is the actual configuration I use, abridged to the parts that matter.
services:
n8n:
image: n8nio/n8n:${N8N_VERSION:-2.37.11}
container_name: n8n
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- n8n_data:/home/node/.n8n
- ./config/n8n:/home/node/.n8n/custom:ro
ports:
- ${N8N_PORT:-5678}:5678
environment:
- N8N_HOST=${N8N_HOST:-localhost}
- N8N_PORT=5678
- N8N_PROTOCOL=${N8N_PROTOCOL:-http}
- NODE_ENV=production
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_USER=${POSTGRES_USER:-n8n}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}
- DB_POSTGRESDB_POOL_SIZE=${DB_POOL_SIZE:-3}
- EXECUTIONS_DATA_PRUNE=${EXECUTIONS_DATA_PRUNE:-true}
- EXECUTIONS_DATA_MAX_AGE=${EXECUTIONS_DATA_MAX_AGE:-336}
- EXECUTIONS_DATA_PRUNE_TIMEOUT=${EXECUTIONS_DATA_PRUNE_TIMEOUT:-3600000}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY:?N8N_ENCRYPTION_KEY is required}
- N8N_USER_FOLDER=/home/node/.n8n
- REDIS_ENABLED=${REDIS_ENABLED:-true}
- REDIS_HOST=redis
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_PASSWORD=${REDIS_PASSWORD:?REDIS_PASSWORD is required}
- EXECUTIONS_MODE=${EXECUTIONS_MODE:-queue}
- N8N_METRICS=${N8N_METRICS:-false}
healthcheck:
test:
- CMD
- wget
- --spider
- http://localhost:5678/healthz
interval: 30s
timeout: 10s
retries: 3
networks:
- n8n_network
Excerpt — n8n from the compose file. The remaining services (proxies, init jobs, exporters) follow the same pattern and mount their configuration from a config/ directory.
Copy .env.example to .env and at minimum set:
N8N_VERSION=latest
N8N_HOST=n8n.example.com
N8N_PORT=5678
N8N_PROTOCOL=https
N8N_ENCRYPTION_KEY=super_secret_encryption_key_here_32_bytes_long
N8N_METRICS=false
.env before the first start — never ship the example valuesUnderstanding the path explains the tuning knobs. A trigger fires in the main process, which serializes the execution as a job and pushes it onto Redis. An idle worker picks the job, loads the workflow definition from PostgreSQL, runs every node, and writes execution data (steps, input, output, timing) back to the database. The UI never runs your logic - it only reads status from PostgreSQL and enqueues new jobs. That is why a heavy workflow slows nothing but its own execution, why a crashed worker loses only the job it was holding (Redis redelivers), and why throughput scales with worker count times the concurrency per worker rather than with one bigger machine.
Start with one worker at the default concurrency and watch two numbers: queue depth (jobs waiting in Redis) and execution duration percentiles. Queue depth that returns to zero between spikes means you are fine; a depth that climbs monotonically during business hours is a sizing signal - add a second worker rather than raising concurrency past what your Postgres and upstream APIs tolerate. Each worker is one Node process, so the memory bill is linear and predictable: roughly the base image footprint plus the data your largest workflows hold in memory.
The compose file demands three secrets before it will even start - that is
the floor, not the ceiling. Generate each with openssl rand -base64 32,
never reuse one across services, and keep them in the .env with 600
permissions, which the stack reads but no backup job should. The encryption
key deserves special handling: it is the only secret whose loss destroys
data rather than access, so it belongs in your password manager and in one
cold copy, not just on the server. Rotate the Redis and Postgres passwords on
a yearly cadence - a five-minute compose restart beats explaining a leak.
Why queue mode instead of n8n's default regular mode? In regular mode the web UI and every workflow execution share one process, so a heavy workflow can freeze the editor. Queue mode pushes executions onto Redis and lets separate worker processes consume them: the UI stays responsive under load, and adding capacity means adding workers, not buying a bigger box.
Why PostgreSQL instead of the default SQLite? SQLite allows exactly one writer at a time, which collides with several workers writing execution data concurrently. PostgreSQL handles the concurrent writes, gives you point-in-time recovery tooling, and makes database-level backups a solved problem.
What is N8N_ENCRYPTION_KEY and why is it required here?
n8n encrypts stored credentials with this key; it is not a password but the
root of the credential vault. The compose file refuses to start without it
(the :? syntax) because starting with a random key would leave previously
saved credentials undecryptable. Back it up - losing it means losing all
credentials.
How does execution data keep from filling the disk?
Execution pruning is enabled with EXECUTIONS_DATA_PRUNE and
EXECUTIONS_DATA_MAX_AGE of 336 hours - two weeks. Successful-run data older
than that is deleted automatically; error data is worth forwarding to a
monitoring system rather than keeping forever in n8n.
A guide, not a product. This page is deployment documentation with working examples — there is no zip, no download, no support contract. You adapt the patterns to your own environment, and you own the result.
Want this running production-grade in your infrastructure instead? I teach this as the one-day n8n Automation workshop — details on the consulting page.