← Back home

Scaling Your Self-Hosted n8n: Queue Mode, Postgres & Custom Docker Images

Scaling Your Self-Hosted n8n: Queue Mode, Postgres & Custom Docker Images

Most teams start their n8n self hosted journey with a simple Docker container β€” and hit a wall the moment workflows multiply. This guide picks up where basic setup leaves off, walking you through the architecture decisions that actually matter at scale: a Postgres-backed data layer, Redis-powered queue mode for horizontal worker scaling, and custom Docker images built for production workloads.

πŸ“Ί Prefer to watch? Full walkthrough on YouTube


What You’ll Learn

By the end of this n8n self hosting tutorial, you’ll have a scalable, production-ready n8n stack with:

This is Part 1 of a series. Future parts will cover taking this stack to the cloud using infrastructure-as-code tools like Pulumi and Terraform.


Why Scale Your n8n Deployment?

The default n8n docker setup β€” a single container with a local SQLite volume β€” works fine for personal use or early prototyping. But it has a hard ceiling. Every workflow execution runs in the same process as the editor, a single database file handles all your state, and there’s no way to add capacity without downtime.

Here’s what a properly scaled n8n self hosted setup unlocks:


Prerequisites


Step 1: Establish Your Local Baseline

Before scaling anything, you need a working local container to build on. This step is intentionally minimal β€” the goal isn’t to run n8n this way in production, but to confirm the image works and understand the default configuration before we layer on scalability components.

The fastest way to get started with n8n docker is to pull the official image and run it directly.

docker run -p 5678:5678 -v ./data_n8n:/home/node/.n8n n8nio/n8n:1.107.2

A few things to note here:

Once it’s running, navigate to http://localhost:5678 and you should see the n8n editor. That’s your first milestone done.


Step 2: Replace SQLite with Postgres

This is the first real scalability step. SQLite β€” n8n’s default database β€” is a single-file store that can’t handle concurrent writes from multiple workers. Before queue mode can work, you need Postgres in place as the shared data layer that both the editor and workers read from and write to simultaneously.

We’ll also move from raw docker run commands to a docker-compose.yml file, which makes managing a multi-service stack significantly easier.

docker-compose.yml (initial structure)

services:
  n8n:
    image: n8nio/n8n:1.107.2
    container_name: dac-n8n-editor
    ports:
      - "5678:5678"
    env_file:
      - .env
    volumes:
      - ./data_n8n:/home/node/.n8n

  postgres:
    image: postgres:16
    container_name: dac-postgres
    ports:
      - "5432:5432"
    env_file:
      - .env
    volumes:
      - ./data_postgres:/var/lib/postgresql/data

.env file

# n8n
N8N_RUNNERS_ENABLED=true
N8N_ENCRYPTION_KEY=your-random-key-here
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true

# Database
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n_user
DB_POSTGRESDB_PASSWORD=your-secure-password

# Postgres credentials
POSTGRES_DB=n8n
POSTGRES_USER=n8n_user
POSTGRES_PASSWORD=your-secure-password

Tip: Pay attention to any warnings that n8n logs on startup. They exist for good reason β€” address them one by one. The N8N_RUNNERS_ENABLED, encryption key, and settings file permissions flags are all recommended by n8n for production setups.

Optional: Add a Makefile for convenience

up:
	docker compose down && docker compose up -d

down:
	docker compose down

Running make up cleanly shuts down existing containers before spinning everything back up, which prevents state conflicts during development.


Step 3: Enable Queue Mode for Horizontal Scaling

This is where n8n self hosted goes from β€œit works” to β€œit scales.” Queue mode separates the editor (the UI) from workers (execution engines), allowing you to run multiple workers in parallel.

How Queue Mode Works

Here’s the execution flow once queue mode is active:

  1. A workflow is triggered β€” the editor creates an execution ID in Redis and stores the workflow data in Postgres
  2. Available workers pick up execution IDs from Redis and fetch the corresponding workflow data from Postgres
  3. Workers process the workflow, then update both Postgres (results) and Redis (status)
  4. The editor polls Redis for execution IDs marked as finished and updates the UI

This architecture means you can add more worker containers as your workload grows without touching the editor service.

Updated docker-compose.yml with Redis and Worker

services:
  n8n-editor:
    image: n8nio/n8n:1.107.2
    container_name: dac-n8n-editor
    ports:
      - "5678:5678"
    env_file:
      - .env

  n8n-worker:
    image: n8nio/n8n:1.107.2
    container_name: dac-n8n-worker
    command: worker
    env_file:
      - .env
    depends_on:
      - redis
      - postgres

  redis:
    image: redis:7
    container_name: dac-redis

  postgres:
    image: postgres:16
    container_name: dac-postgres
    env_file:
      - .env
    volumes:
      - ./data_postgres:/var/lib/postgresql/data
      - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql

Queue mode environment variables (add to .env)

EXECUTIONS_MODE=queue
QUEUE_BULL_REDIS_HOST=redis
QUEUE_BULL_REDIS_PORT=6379

init-db.sql β€” ensure the database exists before n8n starts

One issue you’ll run into: n8n and the worker will try to connect to Postgres before the database is fully initialized. Fix this with a simple SQL init script:

SELECT 'CREATE DATABASE n8n'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'n8n');

Mount this in your Postgres service as shown above. The container will execute it on first startup, guaranteeing the database is ready before your n8n services come online.


Step 4: Build a Custom n8n Docker Image

The stock n8n image is solid, but you’ll eventually want to install additional npm modules or custom nodes. The clean way to do this in an n8n docker setup is to build your own image on top of the official one.

Dockerfile

FROM n8nio/n8n:1.107.2

# Label your build for easy introspection
LABEL com.myn8n.build.version="1.0.0"
LABEL com.myn8n.build.type="custom"

# Set the custom extensions path
ENV N8N_CUSTOM_EXTENSIONS="/home/node/.n8n/custom"

# Install your additional modules
RUN cd /usr/local/lib/node_modules/n8n && \
    npm install global-constants

USER node

Add a build service to docker-compose.yml

services:
  n8n-build:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        N8N_VERSION: 1.107.2
    image: my-custom-n8n:1.107.2
    profiles:
      - build

Build your image:

docker compose --profile build build

Then update your n8n-editor and n8n-worker services to use my-custom-n8n:1.107.2 instead of the base image.

Verify your custom image

# Check your build labels
echo "Build version: $(docker inspect dac-n8n-editor --format='{{index .Config.Labels "com.myn8n.build.version"}}')"
echo "Build type: $(docker inspect dac-n8n-editor --format='{{index .Config.Labels "com.myn8n.build.type"}}')"

# Confirm installed modules
docker exec dac-n8n-editor npm list | grep n8n-nodes

If everything went correctly, your new module (e.g., global-constants) will now appear in the n8n editor’s node palette.


Full Architecture Summary

Here’s what you’ve built:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Docker Compose Stack            β”‚
β”‚                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚  n8n Editor  β”‚    β”‚   n8n Worker(s)  β”‚   β”‚
β”‚  β”‚  :5678       β”‚    β”‚   (scalable)     β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚         β”‚                    β”‚              β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚    Redis     β”‚    β”‚    Postgres      β”‚   β”‚
β”‚  β”‚  (job queue) β”‚    β”‚  (workflow data) β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
ComponentRole
n8n EditorUI + workflow management
n8n WorkerWorkflow execution (scale horizontally)
RedisJob queue + execution status tracking
PostgresPersistent workflow + execution data

What’s Next

You now have a horizontally scalable n8n self hosted stack running locally β€” the same architectural pattern used in production cloud deployments. The gap between what you’ve built here and a cloud-hosted version is smaller than you’d think.

In the next part of this series, we’ll lift this exact stack to the cloud:


Quick Reference Commands

# Start the stack
make up

# Stop the stack
make down

# Check running containers
docker ps

# Tail n8n editor logs
docker logs -f dac-n8n-editor

# Tail worker logs
docker logs -f dac-n8n-worker

# Verify custom image labels
docker inspect dac-n8n-editor --format='{{json .Config.Labels}}'

Troubleshooting

Containers crash on startup Check that your .env file has all required Postgres credentials and that DB_POSTGRESDB_HOST matches your Postgres service name in Docker Compose.

Workflows not executing in queue mode Confirm EXECUTIONS_MODE=queue is set, Redis is reachable, and the worker container started successfully. Check worker logs for connection errors.

Custom modules not appearing in the editor Verify N8N_CUSTOM_EXTENSIONS is set correctly in your Dockerfile and that the module installed without errors during the image build.

Permission errors on the data volume Run chmod 777 ./data_n8n on the host machine before starting containers.


Found this useful? The next post in this series covers cloud deployment with infrastructure-as-code β€” taking the scalable n8n self hosted stack you’ve built here and running it in production.