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

ComponentMinimum (Staging / Low Scale)Recommended (Production: 500k–1M+ tasks/mo)
Compute / VPS2 vCPU, 4 GB RAM4 to 8 vCPU, 8 to 16 GB RAM
Storage Engine20 GB NVMe SSD50+ GB NVMe with automated S3 snapshot backups
Operating SystemUbuntu 22.04 / Debian 12Ubuntu 24.04 LTS / Alpine Linux Minimal
Software StackDocker Engine 26+, Docker Compose v2Docker 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

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *