Skip to content

Instantly share code, notes, and snippets.

@sarenodev
Created March 3, 2026 15:09
Show Gist options
  • Select an option

  • Save sarenodev/0f5b773ff88db8e8702369e1d9934326 to your computer and use it in GitHub Desktop.

Select an option

Save sarenodev/0f5b773ff88db8e8702369e1d9934326 to your computer and use it in GitHub Desktop.
n8n Production Setup — Queue Mode with PostgreSQL & Redis (Docker Compose)

n8n Production Setup — Queue Mode with PostgreSQL & Redis (Docker Compose)

A production-ready n8n stack running in queue mode with PostgreSQL for persistence and Redis as the job queue. Executions are offloaded from the main server to dedicated worker containers.

Architecture

           ┌─────────────┐
  Browser  │  n8n_server │  (UI + webhook receiver)
   ──────► │   :5678     │
           └──────┬──────┘
                  │ enqueues jobs
           ┌──────▼──────┐
           │    Redis     │  (Bull queue)
           └──────┬──────┘
                  │ dequeues jobs
      ┌───────────┴───────────┐
      │                       │
┌─────▼──────┐         ┌──────▼─────┐
│ n8n_worker_1│         │n8n_worker_2│
└─────┬──────┘         └──────┬─────┘
      └───────────┬───────────┘
           ┌──────▼──────┐
           │  PostgreSQL  │  (workflow & execution data)
           └─────────────┘

Before You Start

  1. Change the encryption key — replace N8N_ENCRYPTION_KEY in base-config.yaml with a secret of your own:

    openssl rand -base64 24

    Losing this key means losing access to all stored credentials.

  2. Change the database credentials — update POSTGRES_USER, POSTGRES_PASSWORD, and matching DB_POSTGRESDB_* vars.

  3. Set your domain — replace n8n.example.com in compose.yml with your actual domain.

  4. Put n8n behind a reverse proxy — the server only binds to 127.0.0.1:5678. Use nginx or Caddy to terminate TLS and proxy to it.

base-config.yaml

services:
  n8n-base:
    image: docker.n8n.io/n8nio/n8n:2.11.0
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "20m"
        max-file: "10"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:5678/healthz || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=postgresuser
      - DB_POSTGRESDB_PASSWORD=postgrespw
      - QUEUE_HEALTH_CHECK_ACTIVE=true
      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
      - EXECUTIONS_MODE=queue
      - QUEUE_BULL_REDIS_HOST=redis
      - N8N_RUNNERS_ENABLED=true
      - N8N_RUNNERS_MODE=internal
      - N8N_ENCRYPTION_KEY=dzyOcPr84PyVwOsYPcfrCQ2jYz76BNq+
      - N8N_LOG_LEVEL=debug
      - N8N_LOG_OUTPUT=console
      - CODE_ENABLE_STDOUT=true
      - N8N_BLOCK_ENV_ACCESS_IN_CODE=true
      - N8N_PORT=5678
      - N8N_DEFAULT_BINARY_DATA_MODE=database
      - EXECUTIONS_DATA_PRUNE=true
      - EXECUTIONS_DATA_MAX_AGE=168
      - EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000

compose.yml

services:
  postgres:
    image: postgres:18-alpine
    container_name: postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgresuser
      POSTGRES_PASSWORD: postgrespw
      POSTGRES_DB: n8n
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgresuser -d n8n"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s

  redis:
    image: redis:7-alpine
    container_name: redis
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - ./redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  n8n_server:
    extends:
      file: base-config.yaml
      service: n8n-base
    container_name: n8n_server
    ports:
      - "127.0.0.1:5678:5678"
    volumes:
      - ./n8n_data:/home/node/.n8n
    environment:
      - OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
      - N8N_EDITOR_BASE_URL=https://n8n.example.com
      - WEBHOOK_URL=https://n8n.example.com/
      - N8N_HOST=n8n.example.com
      - N8N_PROTOCOL=https
      - N8N_PROXY_HOPS=1
      - N8N_DIAGNOSTICS_ENABLED=false

  n8n_worker_1:
    extends:
      file: base-config.yaml
      service: n8n-base
    container_name: n8n_worker_1
    command: worker
    environment:
      - NODE_FUNCTION_ALLOW_BUILTIN=*

  n8n_worker_2:
    extends:
      file: base-config.yaml
      service: n8n-base
    container_name: n8n_worker_2
    command: worker
    environment:
      - NODE_FUNCTION_ALLOW_BUILTIN=*

Run

docker compose up -d

Notable Configuration

Setting Value Why
EXECUTIONS_MODE queue Workers pull jobs from Redis instead of the server executing them
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS true Manual test runs also go to workers, keeping the server responsive
N8N_RUNNERS_MODE internal Task runners run inside the worker process (no extra containers needed)
N8N_DEFAULT_BINARY_DATA_MODE database Binary data stored in Postgres, safe for multi-container setups
EXECUTIONS_DATA_PRUNE true Auto-prune executions older than 7 days or over 10k records
N8N_BLOCK_ENV_ACCESS_IN_CODE true Prevents workflows from reading host env vars

Scaling Workers

Add more workers by duplicating a worker block in compose.yml:

  n8n_worker_3:
    extends:
      file: base-config.yaml
      service: n8n-base
    container_name: n8n_worker_3
    command: worker
    environment:
      - NODE_FUNCTION_ALLOW_BUILTIN=*

Then apply:

docker compose up -d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment