n8n Docker Node Deployment: Port, Volume & Compose Configuration
⚡ n8n Workflow Automation T4 · Docker Node Deployment
n8n Docker Node Deployment: Port, Volume & Compose Configuration

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].

4
Core Services (n8n / worker / Postgres / Redis) [1]
127.0.0.1:5678
n8n Bind Address [3]
3
Persistent Volumes [4]
unless-stopped
Restart Policy [5]

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.

💾 Volume Checklist: 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.

🔒 Production Firewall Checklist: (1) 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

This guide is for informational purposes only. Docker image tags, package versions, and environment variable defaults may change. Always refer to the official n8n hosting documentation, Docker documentation, and Nginx documentation for the most current configuration reference.

Leave a Reply

Your email address will not be published. Required fields are marked *