Skip to main content
← Journal·AI AutomationsOct 7, 2026·12 min read

n8n Docker Compose Setup: Production-Ready Stack (2026)

An n8n Docker Compose setup for production: Postgres, Redis queue mode with a worker, task runners and Caddy auto-HTTPS, with an env-var table and a backup job.

A

Founder of IImagined.ai

Quick answer

A production n8n Docker Compose stack needs Postgres instead of SQLite, a pinned n8n version, HTTPS through a reverse proxy, and N8N_WEBHOOK_URL plus N8N_PROXY_HOPS set. This guide gives one compose file with Postgres 18, Redis queue mode with a worker, external task runners and Caddy auto-HTTPS, assembled from n8n's official examples, plus the env-var table, a nightly backup job and fixes for the common failures.

To run n8n with Docker Compose in production, put n8n, Postgres, Redis, a worker, two task runner containers and Caddy in one compose file, pin the n8n version, and set N8N_WEBHOOK_URL and N8N_PROXY_HOPS so webhooks work behind the proxy. The file below does exactly that, and Caddy fetches and renews the HTTPS certificate on its own.

Commands and config checked October 2026 against n8n's Docker Compose install guide, Hetzner guide (the Caddy setup), withPostgresAndWorker example, queue mode, task runner and backup docs, and Caddy's reverse_proxy docs. The current stable n8n release on those pages was 2.42.4.

Existing guides tend to stop at a single n8n container on SQLite, skip worker mode, or leave HTTPS as an exercise. This one is a single copy-paste stack. It is not the only valid layout: n8n's own server guide uses Traefik, and the one-line installer is quicker for a laptop. If you are still deciding whether to self-host at all, read our n8n Cloud vs self-hosted guide first, and for ideas on what to run once it is up, our n8n hub with workflow templates.

What you will have at the end

How a webhook moves through the stack
  1. 01
    Caddy

    Terminates HTTPS on 443, renews the certificate, forwards to n8n:5678.

  2. 02
    n8n main

    Serves the editor, receives webhooks and schedules, creates an execution.

  3. 03
    Redis

    Holds the queue of pending execution IDs.

  4. 04
    n8n worker

    Picks up the job, runs the workflow, sends Code nodes to its task runner.

  5. 05
    Postgres

    Stores workflows, credentials, executions and binary data.

Process flow from n8n's queue mode docs, checked October 2026.

Seven containers: Caddy, Postgres, Redis, the n8n main instance, its task runner, one worker and the worker's task runner. Task runners are the isolation layer for Code node scripts; n8n's task runner docs say to always use external mode in production and that in queue mode each worker needs its own runner sidecar.

Prerequisites

Pre-flight checklist
  • A Linux server with a public IP and SSH key login
  • Docker Engine and the Compose v2 plugin (docker compose version prints a version)
  • A subdomain such as n8n.example.com with an A record pointing at the server
  • Ports 80 and 443 open to the internet, SSH open to you, nothing else
  • At least 4 GB RAM and 2 vCPUs for this seven-container stack (our sizing, not n8n's)
  • A password manager or vault for four generated secrets
  • Somewhere off the server to copy nightly backups to

On sizing: n8n's Hetzner guide says its smallest CPX11 type is enough for a single n8n container at most usage levels, and asks for 4 GB RAM and 2 vCPUs once you add the n8n Assistant sandbox. A worker, Redis and two runners add overhead, so we would not start this stack below 4 GB. What your server costs is covered in our n8n pricing guide.

Self-host n8n with Docker: the steps

Step 1: install Docker and open the firewall

Install Docker Engine and the Compose plugin from Docker's install docs for your distribution, then allow SSH, HTTP and HTTPS. Allow SSH before enabling the firewall or you lock yourself out.

docker --version
docker compose version

sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw enable

Expected result: both version commands print a version, and sudo ufw status lists 22, 80 and 443.

Step 2: create the folder and secrets

sudo mkdir -p /opt/n8n && sudo chown $USER /opt/n8n && cd /opt/n8n

# Run four times; paste each value into .env
openssl rand -hex 32
# .env  (chmod 600 .env, and never commit it)
N8N_VERSION=2.42.4
DOMAIN=n8n.example.com
GENERIC_TIMEZONE=Europe/Berlin

POSTGRES_USER=pgadmin
POSTGRES_PASSWORD=paste-secret-1
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=n8n
POSTGRES_NON_ROOT_PASSWORD=paste-secret-2

ENCRYPTION_KEY=paste-secret-3
RUNNERS_AUTH_TOKEN=paste-secret-4

Store ENCRYPTION_KEY in your password manager as well as on the server. n8n's backup guide is explicit that without the encryption key, a restored database's credentials cannot be decrypted. Check the current stable version on n8n's releases page before you pin one.

Step 3: create the Caddyfile

n8n.example.com {
    reverse_proxy n8n:5678 {
        flush_interval -1
    }
}

This is the Caddyfile from n8n's Hetzner guide, with your domain in place of the placeholder. flush_interval -1 streams responses straight through. Caddy's docs say it serves public domain names over HTTPS with certificates from Let's Encrypt or ZeroSSL, redirects HTTP to HTTPS, and sets the X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host headers by default, which is what n8n's reverse proxy page asks the last proxy to send.

Step 4: create init-data.sh

From n8n's withPostgresAndWorker example. It runs once, on the first Postgres start, and creates the non-root user n8n connects with.

#!/bin/bash
set -e;

if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
    CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
    GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
    GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
EOSQL
else
  echo "SETUP INFO: No Environment variables given!"
fi
chmod +x init-data.sh

Step 5: create compose.yaml

This merges n8n's withPostgresAndWorker file with the Caddy service and the domain, proxy and binary-data settings. Changes from the official example: Caddy in front, n8n's port not published to the host, N8N_HOST, N8N_WEBHOOK_URL, N8N_PROXY_HOPS and timezone added, and binary data stored in the database because queue mode does not support filesystem storage.

volumes:
  caddy_data:
  caddy_config:
  db_storage:
  n8n_storage:
  redis_storage:

x-shared: &shared
  restart: always
  image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
  environment:
    - DB_TYPE=postgresdb
    - DB_POSTGRESDB_HOST=postgres
    - DB_POSTGRESDB_PORT=5432
    - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
    - DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
    - DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
    - EXECUTIONS_MODE=queue
    - QUEUE_BULL_REDIS_HOST=redis
    - QUEUE_HEALTH_CHECK_ACTIVE=true
    - OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
    - N8N_ENCRYPTION_KEY=${ENCRYPTION_KEY}
    - N8N_RUNNERS_MODE=external
    - N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
    - N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
    - N8N_DEFAULT_BINARY_DATA_MODE=database
    - N8N_HOST=${DOMAIN}
    - N8N_PORT=5678
    - N8N_PROTOCOL=https
    - N8N_WEBHOOK_URL=https://${DOMAIN}/
    - N8N_PROXY_HOPS=1
    - NODE_ENV=production
    - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
    - TZ=${GENERIC_TIMEZONE}
  volumes:
    - n8n_storage:/home/node/.n8n
  depends_on:
    redis:
      condition: service_healthy
    postgres:
      condition: service_healthy

x-runner: &runner
  restart: always
  image: n8nio/runners:${N8N_VERSION}

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - caddy_data:/data
      - caddy_config:/config
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
    depends_on:
      - n8n

  postgres:
    image: postgres:18
    restart: always
    environment:
      - POSTGRES_USER
      - POSTGRES_PASSWORD
      - POSTGRES_DB
      - POSTGRES_NON_ROOT_USER
      - POSTGRES_NON_ROOT_PASSWORD
      - PGDATA=/var/lib/postgresql/data
    volumes:
      - db_storage:/var/lib/postgresql/data
      - ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
      interval: 5s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7-alpine
    restart: always
    volumes:
      - redis_storage:/data
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    <<: *shared

  n8n-runner:
    <<: *runner
    environment:
      - N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
      - N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
    depends_on:
      - n8n

  n8n-worker:
    <<: *shared
    command: worker
    depends_on:
      - n8n

  n8n-worker-runner:
    <<: *runner
    environment:
      - N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
      - N8N_RUNNERS_TASK_BROKER_URI=http://n8n-worker:5679
    depends_on:
      - n8n-worker

Step 6: start the stack

docker compose up -d
docker compose ps
docker compose logs -f n8n

Expected result: postgres and redis show healthy, the other five show running, and the n8n log reports that the editor is accessible. The first start takes a minute while n8n migrates the empty database and Caddy requests the certificate.

Step 7: verify HTTPS, the worker and the runners

curl -sf https://n8n.example.com/healthz && echo OK
docker compose logs n8n-worker | tail -20
docker compose logs n8n-worker-runner | tail -20

Expected result: OK, a worker log that shows it connected and is waiting for jobs, and a runner log without authentication errors. Then build a two-node test workflow with a Code node and run it from the editor: with manual executions offloaded to workers, a successful run proves the worker, its runner and Redis are all wired up.

Step 8: claim the owner account straight away

Open your domain and create the owner account immediately. Until you do, anyone who reaches the URL sees the setup screen, and n8n's backup docs note that the first person to complete owner setup becomes the owner. Then turn on two-factor authentication in your personal settings.

The whole procedure at a glance
  1. 1
    Docker and firewall

    Compose v2 installed; 22, 80 and 443 open.

  2. 2
    Folder and .env

    Four secrets generated; encryption key saved off the server.

  3. 3
    Caddyfile

    Your domain, reverse_proxy to n8n:5678.

  4. 4
    init-data.sh

    Creates the non-root Postgres user on first start.

  5. 5
    compose.yaml

    Seven services, version pinned.

  6. 6
    Start and verify

    Healthy containers, /healthz returns OK over HTTPS.

  7. 7
    Owner account and 2FA

    Before anyone else finds the URL.

  8. 8
    Backups

    Nightly job below, copied off the server.

The environment variables that matter

VariableWhat it doesWhy it matters
N8N_VERSIONPins n8n and the runners image to one release, e.g. 2.42.4Unpinned images upgrade on every pull, including across breaking changes
DOMAINYour n8n hostname, e.g. n8n.example.comFeeds N8N_HOST and N8N_WEBHOOK_URL; must match the Caddyfile
ENCRYPTION_KEYEncrypts saved credentials; shared by main and workerLose it and every stored credential is unreadable
POSTGRES_USER / POSTGRES_PASSWORDPostgres superuser, used only for setup and backupsKeep out of the n8n containers
POSTGRES_NON_ROOT_USER / _PASSWORDThe user n8n connects as, created by init-data.shLeast privilege for the app
RUNNERS_AUTH_TOKENShared secret between n8n and the task runner containersCode nodes fail if it does not match
GENERIC_TIMEZONETimezone for schedules; defaults to America/New_YorkSchedules fire at the wrong hour otherwise
N8N_WEBHOOK_URLPublic base URL for webhooks behind the proxyReplaces WEBHOOK_URL, deprecated from n8n 2.35.0
N8N_PROXY_HOPS=1Tells n8n one proxy (Caddy) sits in frontClient IPs and protocol detection break without it
N8N_DEFAULT_BINARY_DATA_MODE=databaseStores files in Postgres so main and worker both see themQueue mode does not support filesystem binary storage

References: n8n's deployment, endpoint and binary data variable pages, checked October 2026. Database-mode binary storage caps a single file at 512 MiB by default.

The backup job

n8n's backup guide says a complete backup is the Postgres database plus the .n8n folder, which in Docker lives in the n8n storage volume and holds the encryption key config. The CLI export commands alone miss users, execution history, variables and settings. This script takes all three pieces every night:

#!/usr/bin/env bash
# /opt/n8n/backup.sh  - nightly n8n backup
set -euo pipefail
cd /opt/n8n
set -a; . ./.env; set +a
mkdir -p backups
STAMP=$(date +%F)

# 1. Postgres dump
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
  | gzip > "backups/n8n-db-$STAMP.sql.gz"

# 2. The .n8n folder (check the volume name with: docker volume ls)
docker run --rm -v n8n_n8n_storage:/data:ro -v "$PWD/backups":/backup alpine \
  tar czf "/backup/n8n-storage-$STAMP.tar.gz" -C /data .

# 3. The .env file, which holds the encryption key in this setup
cp .env "backups/env-$STAMP"
chmod 600 backups/*

# 4. Keep 14 days locally; copy off the server with your own tool
find backups -type f -mtime +14 -delete
chmod +x /opt/n8n/backup.sh
crontab -e
# add this line: run at 03:15 every night
15 3 * * * /opt/n8n/backup.sh >> /opt/n8n/backup.log 2>&1

Copy the backups folder off the server with whatever you already use; a backup on the same disk does not survive the disk. Then test a restore on a spare server: stop n8n, restore the volume and the database, restore the .env file, start the stack, and log in. Your error alerts belong in n8n too; our n8n error handling guide shows how to set an error workflow that messages you when something fails.

Updating the stack

./backup.sh
# edit N8N_VERSION in .env, then:
docker compose pull
docker compose up -d

Pinning n8n and the runners image to the same variable matters: n8n's task runner docs say the n8nio/runners version must match n8nio/n8n. n8n releases a minor version most weeks and recommends updating at least monthly. n8n 3.0 is scheduled for October 2026 and removes legacy nodes such as Function and Cron; check the 3.0 migration report in Settings first, and see our n8n 3.0 upgrade guide. Postgres is different: moving an existing volume to a new major version fails with database files are incompatible with server, so follow PostgreSQL's upgrade guide rather than bumping the tag.

Troubleshooting

  • No certificate, browser shows a TLS error. The A record does not point at this server yet, or port 80 or 443 is closed. Check with docker compose logs caddy.
  • Webhook URLs show localhost or port 5678. N8N_WEBHOOK_URL is missing or wrong. Set it to your public HTTPS address and run docker compose up -d.
  • Database is empty after a restart. The PGDATA line is missing; see the warning above.
  • Code node fails or hangs. RUNNERS_AUTH_TOKEN differs between containers, or the runners image version does not match n8n.
  • Credentials cannot be decrypted after a move. The new instance has a different ENCRYPTION_KEY. Restore the original .env.
  • Workflows queue up and run slowly. One worker runs 10 jobs at once by default. Add workers, each with its own runner sidecar, or raise concurrency with worker --concurrency; n8n recommends 5 or more.

Once it is running, the useful next step is building real workflows on it. If you want to turn those workflows into a product that customers pay for, our AI SaaS Builder program covers building and charging for it.

n8n Docker Compose: FAQ

What is the best Docker Compose file for n8n?

For production, use one that runs n8n against Postgres rather than the default SQLite, pins the n8n version, puts a reverse proxy with automatic HTTPS in front, and sets N8N_WEBHOOK_URL and N8N_PROXY_HOPS. Add Redis, a worker and task runner containers when you want queue mode. The file in this guide combines n8n's official withPostgresAndWorker example with the Caddy setup from n8n's Hetzner guide.

Does n8n Docker Compose need Postgres?

No, n8n runs on SQLite by default, and n8n's docs call SQLite fine for trying things out. They recommend Postgres for a production instance with more than a handful of users or workflows running around the clock, and running queue mode on SQLite is not recommended. n8n supports PostgreSQL 16, 17 and 18. Switching later starts with an empty database, so decide before you build.

Is queue mode available on the free n8n Community Edition?

Yes. n8n's edition comparison says queue mode is included in the Community Edition, while multi-main, which runs several main instances for high availability, needs a paid license. Queue mode needs Redis, Postgres, the same encryption key on every instance, and no filesystem binary data storage. Workers default to running 10 jobs at once; n8n recommends a concurrency of 5 or more.

Why use Caddy instead of Traefik for n8n?

Either works; n8n documents both. Its generic Docker Compose server guide uses Traefik, and its Hetzner guide uses Caddy. Caddy gets and renews Let's Encrypt certificates and redirects HTTP to HTTPS by default, and it sets the X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host headers n8n expects behind a proxy, so the config is a four-line Caddyfile.

How do I update n8n running in Docker Compose?

Back up first, read the release notes for breaking changes, then change N8N_VERSION in .env and run docker compose pull followed by docker compose up -d. The n8n and n8nio/runners images must stay on the same version, which pinning both to one variable handles. n8n releases a minor version most weeks and recommends updating at least monthly.

What do I need to back up for a Docker n8n install?

Two things, per n8n's backup guide: the Postgres database and the .n8n folder, which in Docker lives in the n8n storage volume and holds the encryption key config. Keep your .env file too, since this setup sets the encryption key there. Without that key, credentials in a restored database cannot be decrypted. Test a restore on a spare server before you rely on it.

All Access · all four programs · $99/mo

Server's up. Now build on it.

AI SaaS Builder, included in All Access, covers n8n automations, AI agents and shipping a paid AI product, with the other three programs, live coaching and the private community in one subscription.

Start All Access — $99/mo →30-day money-back guarantee
Free · no signup

Setting up your first n8n server?

Grab the free Creator Starter Kit, then browse our n8n hub for workflows worth running on your new instance.

About the author

Written by Anyro, Founder of IImagined.ai. IImagined.ai is a founder-led education platform teaching Instagram growth, AI influencers, digital products, and AI automation.

Results vary; no income is guaranteed.

All-Access subscription

Every program. Member benefits.
One subscription.

Use all four premium programs with weekly live coaching, a private community, and the resource vault.

Confirm current lessons, downloadable resources and member-benefit arrangements before purchasing.

  • All 4 premium programs plus free Futures Trading
  • Weekly live coaching calls
  • Private community access
  • Resource vault and templates
  • 30-day money-back guarantee, cancel anytime
$99/ month
$99 for the first month · $702 to buy all four standalone
Start All-AccessOr browse standalone programs
30-day money-back guarantee · $99/month · cancel anytime