A production n8n deployment runs on Docker Compose with four
containerized services: n8n (the main application), n8n-worker
(queue-mode execution using the same image with the worker command),
PostgreSQL (mandatory for queue mode; SQLite cannot handle
concurrent multi-process writes), and Redis (≥6.0 required as
the message broker via Bull). All services share a Docker network, mount
persistent volumes — /home/node/.n8n for workflows and credentials,
/var/lib/postgresql/data for the database — and bind n8n to
127.0.0.1:5678 so that only the reverse proxy (Nginx) serves HTTPS
traffic on port 443. Every variable toggling the stack lives in a single
.env file, committed nowhere [1]
[2].
How does the queue-mode Docker Compose architecture separate the main process, workers, Postgres, and Redis?
The n8n deployment consists of four containerized services orchestrated via
Docker Compose. The n8n main service provides the web
interface and API endpoint, listening on port 5678 with port mapping
5678:5678 and a restart policy of always. The
n8n-worker service uses the identical Docker image but runs
the worker command — it exposes no ports, has configuration
inherited via YAML anchor <<: *shared, and mounts the same
n8n_storage:/home/node/.n8n volume [1].
The postgres service runs postgres:16-alpine with
its own named volume for data persistence, a healthcheck that runs
pg_isready -U n8n every 10 seconds, and
restart: unless-stopped. The redis service runs
redis:7-alpine with a redis_data volume for dump.rdb
persistence. The startup order is enforced by depends_on:
PostgreSQL and Redis must be healthy before n8n starts; the worker waits for
n8n. This creates the chain: Postgres/Redis → n8n → n8n-worker,
ensuring the message broker and database are available before any workflow
execution is attempted [1].
For a complete breakdown of how queue mode distributes jobs across workers using
Redis as the broker, see the n8n Node Execution Engine guide.
| Service | Image | Port / Expose | Volume Mount | Depends On |
|---|---|---|---|---|
| postgres | postgres:16-alpine |
5432 (internal) | postgres_data:/var/lib/postgresql/data |
— (first to start) |
| redis | redis:7-alpine |
6379 (internal) | redis_data:/data |
— (first to start) |
| n8n | docker.n8n.io/n8nio/n8n |
127.0.0.1:5678:5678 |
n8n_data:/home/node/.n8n |
postgres (healthy), redis (healthy) |
| n8n-worker | docker.n8n.io/n8nio/n8n (command: worker) |
None (internal only) | n8n_data:/home/node/.n8n (shared) |
n8n (started) |
How do you configure the .env file to drive all environment variables across the stack?
The .env file is the single source of truth for the entire
deployment. It keeps secrets out of the Compose file and is never committed
to version control. At minimum, define DOMAIN,
N8N_HOST, N8N_PROTOCOL=https,
WEBHOOK_URL, N8N_ENCRYPTION_KEY (generated
with openssl rand -hex 32), DB_TYPE=postgresdb,
and six DB_POSTGRESDB_* variables. Reference every variable in
docker-compose.yml with ${VAR_NAME} syntax.
[6]
[4]
The encryption key (N8N_ENCRYPTION_KEY) must be identical across
the main process and every worker — mismatched keys prevent workers from
decrypting credentials stored in PostgreSQL. For queue mode, add
QUEUE_BULL_REDIS_HOST=redis and
QUEUE_BULL_REDIS_PORT=6379 plus an optional password. The minimal
set of proxy variables for correct webhook URLs behind a reverse proxy is:
N8N_HOST=${DOMAIN}, N8N_PROTOCOL=https,
N8N_PORT=5678, and
WEBHOOK_URL=https://${DOMAIN}/ [6].
Without these variables correctly set, generated webhook URLs will default to
http://localhost:5678/, causing every third-party callback to
fail silently. For the complete credential encryption reference and key
rotation procedure, see the n8n Credential Nodes guide.
| Variable | Required | Example Value | Purpose |
|---|---|---|---|
DOMAIN |
✅ | n8n.example.com |
Public domain for HTTPS & webhook URL generation |
N8N_HOST |
✅ | ${DOMAIN} |
UI & webhook URL hostname |
N8N_PROTOCOL |
✅ | https |
Protocol for generated webhook URLs |
WEBHOOK_URL |
✅ | https://${DOMAIN}/ |
Explicit override for webhook base URL |
N8N_PORT |
Optional | 5678 |
HTTP port n8n listens on |
N8N_PROXY_HOPS |
✅ | 1 |
Trust one reverse proxy in front |
N8N_ENCRYPTION_KEY |
✅ | openssl rand -hex 32 |
Encrypts credentials at rest; must match on all workers |
DB_TYPE |
✅ | postgresdb |
Database driver; required for queue mode |
How do you configure persistent volumes so workflows and credentials survive container restarts?
n8n stores all persistent state under /home/node/.n8n — workflow
definitions, encrypted credentials, execution history, and the encryption key.
Without a volume mount, every Docker restart creates a fresh n8n instance with
an empty SQLite database. Mount a named volume:
n8n_data:/home/node/.n8n. For PostgreSQL, mount a separate volume:
postgres_data:/var/lib/postgresql/data. For Redis, mount
redis_data:/data to persist the dump.rdb snapshot.
[4]
[7]
A critical operational detail: never use docker compose down on
production — it removes containers and anonymous volumes by default. Use
docker compose stop and docker compose start
instead. Named volumes defined at the top level of the Compose file persist
regardless of container state. Verify volumes exist with docker volume ls.
For the n8n data volume, ownership must match UID 1000 (the node user inside
the container): sudo chown -R 1000:1000 /path/to/n8n_data [4].
The n8n-worker service mounts the same n8n_data volume
as the main n8n service, ensuring workers have access to the identical encryption
key and configuration. For automated daily backups to remote storage, see the
n8n Database guide.
n8n_data:/home/node/.n8n
(workflows, credentials, encryption key). postgres_data:/var/lib/postgresql/data
(execution history, workflow definitions). redis_data:/data (queue
persistence via dump.rdb). All three must be named volumes defined at the top
level of docker-compose.yml. Never use anonymous volumes or bind mounts without
explicit paths — data loss on container removal is guaranteed.
[4]
How do you bind n8n to localhost:5678 and configure Nginx as a secure HTTPS reverse proxy?
Bind n8n to the loopback interface with
ports: "127.0.0.1:5678:5678" — this means n8n only listens on
localhost, and all external traffic must pass through the reverse proxy. Never
use 5678:5678 alone (which binds to 0.0.0.0:5678)
because it exposes n8n directly to the internet [3].
The reverse proxy (Nginx or Caddy) terminates HTTPS on port 443, then forwards
to http://n8n:5678 via Docker's internal DNS.
[8]
For Nginx, configure a server block that listens on port 443 with
SSL certificates (via Let's Encrypt), then add
proxy_pass http://n8n:5678; plus four essential proxy headers:
proxy_set_header Host $host;,
proxy_set_header X-Forwarded-Proto $scheme;,
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;, and
proxy_set_header X-Forwarded-Host $host;. Set
proxy_read_timeout 3600s for long-running workflows. For WebSocket
support (required for editor live-reload and real-time execution monitoring),
add proxy_set_header Upgrade $http_upgrade; and
proxy_set_header Connection "upgrade";. Then set
N8N_PROXY_HOPS=1 so n8n trusts the proxy headers and correctly
generates webhook URLs with https://. For Caddy, the configuration
is a single line: reverse_proxy n8n:5678 — TLS is auto-negotiated
with Let's Encrypt. For the complete reverse-proxy production checklist covering
DDoS protection headers, rate limiting, and IP filtering, see the
n8n Node Security Hardening guide.
| Header | Value | Required | Without It |
|---|---|---|---|
X-Forwarded-For |
$proxy_add_x_forwarded_for |
✅ Yes | All IPs show as 127.0.0.1 |
X-Forwarded-Proto |
$scheme |
✅ Yes | Webhook URLs default to http:// |
X-Forwarded-Host |
$host |
✅ Yes | Broken OAuth redirects |
Host |
$host |
✅ Yes | Webhook URL shows localhost |
How many workers should you deploy, and how do you tune concurrency for production workloads?
The n8n-worker service uses the same Docker image as the main n8n service but
runs the worker command — no port exposure, configuration inherited
via YAML anchor, and the same volume mount for the encryption key. For
CPU‑bound workflows (CSV processing, data transforms), start with
1 worker per available vCPU core and concurrency
5–10. For I/O‑bound workflows (API polling, webhook forwarding),
run 2–3 workers per core with concurrency 15–25.
[2]
[1]
Horizontal scaling in Compose is achieved by adding more n8n-worker
services or using docker compose up -d --scale n8n-worker=3. Every
worker inherits the same shared configuration — they connect to the same Redis
queue and PostgreSQL database, are completely stateless, and can be added or
removed without downtime. BullMQ automatically redistributes failed jobs among
the remaining workers. Set the environment variable
N8N_CONCURRENCY_PRODUCTION_LIMIT as a global ceiling that overrides
per-worker --concurrency flags when set to any value other than
-1 [2].
For the complete scaling guide covering worker-to-core ratios, memory
allocation, and Kubernetes horizontal pod autoscaling patterns, see the
n8n Scaling & Queue Configuration guide.
How do you configure the firewall and lock down the production Docker node after deployment?
Restrict host-level ingress with UFW: open only ports 443/tcp
and 80/tcp, then enable the firewall. Port 5678 must remain closed to external
traffic — it is bound to 127.0.0.1:5678 in the Compose file so
only the reverse proxy on the same host can reach it. At the container level,
block egress from the n8n container to prevent a compromised workflow from
exfiltrating data to arbitrary external servers.
[7]
[2]
The post-deployment hardening checklist: (1) enable Basic Auth with
N8N_BASIC_AUTH_ACTIVE=true, N8N_BASIC_AUTH_USER,
and N8N_BASIC_AUTH_PASSWORD; (2) restrict file access with
N8N_RESTRICT_FILE_ACCESS_TO and
N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES=true; (3) disable high-risk
nodes via NODES_EXCLUDE; (4) set
N8N_PAYLOAD_SIZE_MAX to prevent DoS via large webhook payloads;
(5) pin Docker image tags (n8nio/n8n:2.17.7, not
latest) and use docker compose pull + docker
compose up -d for versioned upgrades. Set Docker logging to
--log-driver=json-file --log-opt max-size=10m --log-opt max-file=3
to prevent disk exhaustion from debug logs. For the complete self-hosted
hardening blueprint covering every environment variable and firewall rule,
see the n8n Node Security Hardening guide.
ufw allow 443/tcp && ufw allow 80/tcp && ufw enable.
(2) Verify port 5678 is closed externally:
nmap -p 5678 your-server-ip. (3) Bind n8n to
127.0.0.1:5678 in docker-compose.yml. (4) Block container
egress with iptables or a Docker network with
internal: true. (5) Set N8N_PAYLOAD_SIZE_MAX
to a sensible limit (e.g., 16 MB).
[7]
References
- DeepWiki — n8n Service Architecture & Components: four containerized services (n8n, n8n-worker, postgres, redis), YAML anchors, startup order, health checks (Mar 2026)
- Contabo — n8n Queue Mode Setup Guide for VPS Scalability: Docker Compose service order (Redis first, then n8n, then workers), production hardware (4+ cores, 8GB+ RAM), worker count and concurrency tuning (Feb 2026)
- n8n Community — Hosting n8n with Docker Compose and Traefik: 127.0.0.1:5678 bind, Traefik labels for routing, NODE_FUNCTION_ALLOW_EXTERNAL for LangChain nodes (Nov 2025)
- dev.to — Self-hosting n8n in 2026: A Production Setup That Doesn't Bite You in Week Two — Compose file, Postgres 16-alpine, persistent volumes, UID 1000 ownership, Hetzner CAX11 ($3.29/mo for 2 vCPU + 4 GB) (May 2026)
- dev.to — How to Self-Host n8n with Docker Compose: restart unless-stopped, GENERIC_TIMEZONE, n8n_data volume, ports 5678:5678 (Apr 2026)
- Royfactory — n8n Self-Hosting with Docker Compose: Production Templates and Ops Checklist — .env variables (N8N_HOST, N8N_PROTOCOL, WEBHOOK_URL), Postgres minimum variables, Redis queue settings, queue mode environment (Jan 2026)
- LumaDock — The Ultimate n8n VPS Self-Hosting Guide: Caddy/Nginx proxy, PostgreSQL vs SQLite, queue mode with Redis, sizing (2 vCPU + 4 GB RAM baseline), backup strategies (Sep 2025)
- LiteSpeed Blog — Proxy n8n with LiteSpeed Web Server: N8N_HOST, N8N_PROTOCOL, WEBHOOK_URL, N8N_EDITOR_BASE_URL, N8N_PROXY_HOPS=1, WebSocket support via rewrite rules (May 2026)
- n8n Community — Webhook URLs Test / Production: N8N_EDITOR_BASE_URL removal fix, WEBHOOK_URL proxy-only behavior, N8N_PROTOCOL/N8N_HOST/N8N_PORT for URL construction (Mar 2026)

