How to Deploy n8n in Queue Mode with Docker Compose, Redis, and PostgreSQL
When running mission-critical automation workflows at scale, the default single-process container of n8n quickly becomes an operational bottleneck. Webhooks queue up in memory, long-running scripts block the single Node.js event loop, and a solitary process crash halts your entire integration pipeline.
To achieve production reliability, zero-downtime scalability, and horizontal worker concurrency, you must deploy n8n in Queue Mode.
Architecture Overview: In Queue Mode, n8n decouples the user interface and orchestrator from actual workflow execution. An ingress proxy handles SSL termination, the primary n8n instance schedules jobs, Redis serves as an in-memory job broker (BullQueue), PostgreSQL persists workflow states, and dedicated worker containers execute tasks concurrently without choking the main UI.
Production Architecture Diagram
<!-- 1. SSL & Ingress -->
<rect x="260" y="20" width="240" height="50" rx="8" fill="#1E293B" stroke="#334155" stroke-width="1.5"/>
<text x="380" y="44" fill="#F8FAFC" font-size="13" font-weight="600" text-anchor="middle">HTTPS Ingress / Webhooks</text>
<text x="380" y="58" fill="#94A3B8" font-size="11" text-anchor="middle">Port 80 / 443 (Let's Encrypt)</text>
<line x1="380" y1="70" x2="380" y2="94" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>
<!-- 2. Traefik Proxy -->
<rect x="260" y="100" width="240" height="50" rx="8" fill="#1E293B" stroke="#38BDF8" stroke-width="1.5"/>
<text x="380" y="124" fill="#38BDF8" font-size="13" font-weight="600" text-anchor="middle">Traefik Reverse Proxy</text>
<text x="380" y="138" fill="#94A3B8" font-size="11" text-anchor="middle">Automated SSL & Host Routing</text>
<line x1="380" y1="150" x2="380" y2="174" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>
<!-- 3. Primary n8n instance -->
<rect x="240" y="180" width="280" height="60" rx="8" fill="#0F172A" stroke="#2563EB" stroke-width="2"/>
<text x="380" y="206" fill="#FFFFFF" font-size="15" font-weight="700" text-anchor="middle">n8n Main (Editor & Scheduler)</text>
<text x="380" y="224" fill="#93C5FD" font-size="12" text-anchor="middle">Internal Webhook Dispatcher</text>
<!-- Paths to DB & Queue -->
<path d="M 320 240 L 320 274" fill="transparent" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>
<path d="M 440 240 L 440 274" fill="transparent" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>
<!-- 4. PostgreSQL -->
<rect x="130" y="280" width="220" height="60" rx="8" fill="#0F172A" stroke="#334155" stroke-width="1.5"/>
<text x="240" y="306" fill="#F8FAFC" font-size="14" font-weight="600" text-anchor="middle">PostgreSQL 16</text>
<text x="240" y="324" fill="#64748B" font-size="12" text-anchor="middle">Workflow Storage & Credentials</text>
<!-- 5. Redis Broker -->
<rect x="410" y="280" width="220" height="60" rx="8" fill="#0F172A" stroke="#DC2626" stroke-width="1.5" stroke-dasharray="4,2"/>
<text x="520" y="306" fill="#F8FAFC" font-size="14" font-weight="600" text-anchor="middle">Redis 7 Alpine</text>
<text x="520" y="324" fill="#F87171" font-size="12" text-anchor="middle">BullQueue Job Dispatcher</text>
<!-- Paths to Worker Nodes -->
<path d="M 240 340 L 240 380 L 250 380 L 250 394" fill="transparent" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>
<path d="M 520 340 L 520 380 L 510 380 L 510 394" fill="transparent" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>
<!-- 6. Worker Nodes -->
<rect x="130" y="400" width="240" height="55" rx="8" fill="#1E293B" stroke="#10B981" stroke-width="1.5"/>
<text x="250" y="424" fill="#34D399" font-size="13" font-weight="600" text-anchor="middle">n8n Worker 01</text>
<text x="250" y="441" fill="#94A3B8" font-size="11" text-anchor="middle">Isolated Execution Sandbox</text>
<rect x="390" y="400" width="240" height="55" rx="8" fill="#1E293B" stroke="#10B981" stroke-width="1.5"/>
<text x="510" y="424" fill="#34D399" font-size="13" font-weight="600" text-anchor="middle">n8n Worker 02</text>
<text x="510" y="441" fill="#94A3B8" font-size="11" text-anchor="middle">Isolated Execution Sandbox</text>
Prerequisites & Hardware Sizing
Before provisioning, ensure you have a clean Linux VPS (Ubuntu 22.04 LTS or 24.04 LTS) and a domain pointed to your server IP via an A record (e.g., n8n.yourdomain.com).
| Component | Minimum (Staging / Low Scale) | Recommended (Production: 500k–1M+ tasks/mo) |
| Compute / VPS | 2 vCPU, 4 GB RAM | 4 to 8 vCPU, 8 to 16 GB RAM |
| Storage Engine | 20 GB NVMe SSD | 50+ GB NVMe with automated S3 snapshot backups |
| Operating System | Ubuntu 22.04 / Debian 12 | Ubuntu 24.04 LTS / Alpine Linux Minimal |
| Software Stack | Docker Engine 26+, Docker Compose v2 | Docker Engine 26+, Docker Compose v2 |
Step 1: Server Hardening & Directory Scaffold
Connect via SSH and prepare the isolated operational directory:
# Update system packages
sudo apt update && sudo apt upgrade -y
# Install Docker and Docker Compose plugin if missing
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Create n8n production project directory
mkdir -p /opt/n8n-cluster
cd /opt/n8n-cluster
# Set secure permissions
mkdir -p local_files
sudo chown -R 1000:1000 local_files
Step 2: Environment Variables (.env)
Generate cryptographically secure passwords and define your operational flags.
Run this command inside /opt/n8n-cluster to generate a secure random 32-character encryption key:
openssl rand -hex 32
Now create your environment configuration file:
nano .env
Paste the following configuration (replace placeholders with your real values):
# Domain & General Config
DOMAIN_NAME=n8n.yourdomain.com
SUBDOMAIN=n8n
SSL_EMAIL=admin@yourdomain.com
GENERIC_TIMEZONE=UTC
# Security & Secrets
POSTGRES_USER=n8n_cluster_admin
POSTGRES_PASSWORD=UseAStrictlyRandomPasswordString981!
POSTGRES_DB=n8n_production
REDIS_PASSWORD=AnotherSuperRandomPasswordStringRedis412!
N8N_ENCRYPTION_KEY=PASTE_YOUR_OPENSSL_OUTPUT_HERE
# Production Execution Pruning (Critical for High Throughput)
EXECUTIONS_DATA_PRUNE=true
EXECUTIONS_DATA_MAX_AGE=168
EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000
Step 3: Production Docker Compose Configuration
Create your orchestration file:
nano docker-compose.yml
Paste the complete multi-container production specification:
version: '3.8'
networks:
traefik_proxy:
external: false
n8n_internal:
internal: true
volumes:
traefik_certificates:
postgres_storage:
redis_storage:
n8n_main_data:
services:
# --- Reverse Proxy with Auto-SSL ---
traefik:
image: traefik:v3.0
restart: always
command:
- "--api.insecure=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=${SSL_EMAIL}"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "traefik_certificates:/letsencrypt"
networks:
- traefik_proxy
# --- State Persistence ---
postgres:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_storage:/var/lib/postgresql/data
networks:
- n8n_internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
# --- Job Broker ---
redis:
image: redis:7-alpine
restart: always
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
volumes:
- redis_storage:/data
networks:
- n8n_internal
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 5s
timeout: 5s
retries: 5
# --- n8n Main (Primary UI & Scheduler) ---
n8n-main:
image: docker.n8n.io/n8nio/n8n:latest
restart: always
environment:
- 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}
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
- QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_HOST=${DOMAIN_NAME}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- NODE_ENV=production
- WEBHOOK_URL=https://${DOMAIN_NAME}/
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- EXECUTIONS_DATA_PRUNE=${EXECUTIONS_DATA_PRUNE}
- EXECUTIONS_DATA_MAX_AGE=${EXECUTIONS_DATA_MAX_AGE}
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=${EXECUTIONS_DATA_PRUNE_MAX_COUNT}
volumes:
- n8n_main_data:/home/node/.n8n
- ./local_files:/files
networks:
- traefik_proxy
- n8n_internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.n8n.rule=Host(`${DOMAIN_NAME}`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls.certresolver=myresolver"
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# --- n8n Worker Instances (Execution Engines) ---
n8n-worker:
image: docker.n8n.io/n8nio/n8n:latest
restart: always
command: /usr/local/bin/n8n worker --concurrency=10
environment:
- 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}
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_BULL_REDIS_PORT=6379
- QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- NODE_ENV=production
volumes:
- ./local_files:/files
networks:
- n8n_internal
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
Step 4: Bootstrapping the Cluster
Deploy the stack in detached background mode:
docker compose up -d
Verifying Service Health
Check that all four containers are running and healthy:
docker compose ps
Monitor incoming worker connections and Traefik certificate issuance in real-time:
docker compose logs -f n8n-main n8n-worker
Once the initial logs report Editor is now accessible via https://..., navigate to [https://n8n.yourdomain.com](https://n8n.yourdomain.com) in your browser. You will be greeted by the owner setup screen.
Step 5: Horizontal Worker Scaling
If your operational workload spikes, you can scale execution nodes without downtime. You do not need to edit your configuration file—simply invoke Docker Compose’s scale command:
docker compose up -d --scale n8n-worker=4
This immediately spawns 4 worker containers listening to the same Redis BullQueue channel, quadrupling your parallel processing capability.
Critical Production Hardening: PostgreSQL Execution Pruning
At a pace of 1,000,000 tasks per month, raw execution logs will consume roughly 40 GB to 100 GB of storage within weeks, grinding PostgreSQL queries to a crawl.
Confirm your pruning flags are active by inspecting your running configuration:
EXECUTIONS_DATA_PRUNE=true: Tells the primary engine to delete completed task history automatically.EXECUTIONS_DATA_MAX_AGE=168: Retains successful logs for 7 days (168 hours) for debugging before purging.EXECUTIONS_DATA_PRUNE_MAX_COUNT=50000: Caps maximum kept rows regardless of age, preventing runaway tables during heavy batch operations.
Frequently Asked Questions
Can I run n8n Queue Mode on a single VPS?
Yes. Queue Mode is primarily an architectural decoupling pattern, not strictly a multi-machine requirement. Running the main instance, Redis, PostgreSQL, and workers on the same VPS protects the main UI and webhook ingestion from running out of memory when large batches execute.
How do I install custom npm packages on worker containers?
Because worker containers handle actual code execution, you must define the NODE_FUNCTION_ALLOW_EXTERNAL environment variable on the n8n-worker service (e.g., NODE_FUNCTION_ALLOW_EXTERNAL=lodash,axios,moment). If a package requires native OS compilation, build a customized Dockerfile inheriting from docker.n8n.io/n8nio/n8n:latest.
What happens to incoming webhooks if all workers are at capacity?
Traefik routes the webhook to the main instance, which writes the job specification into the Redis BullQueue and immediately returns an HTTP 200/201 acknowledgment (or holds if sync response is required). No payloads are lost; they remain persisted in Redis until an available worker picks them up.
How do I update n8n to newer versions safely?
To update without configuration drift, pull the latest images, recreate containers sequentially, and verify schema migrations:
docker compose pull
docker compose up -d