Three complete docker-compose stacks, prerendered and copy-ready, following the official n8n-io/n8n-hosting topology (n8n:stable image, Postgres 16, Redis 7) and targeting n8n 2.x. Pick the one that matches your server, or open it in the Step 2 builder to get every number — concurrency, heap caps, pool size — tuned to your actual load. Every figure on this page comes from n8n's docs, n8n's published benchmark, or community-measured footprints, with the source named inline.
For light-to-moderate automation on a small VPS: one n8n container on the stable image, Postgres 16 as the database (n8n 2.x dropped MySQL; Postgres is the supported server DB), and Caddy terminating TLS with automatic certificates — community measurements (~860 MB idle stack plus 40-50 MB per overlapping light run) put a comfortable single-instance floor around 2 GB. N8N_SECURE_COOKIE (the classic can't-log-in-behind-a-proxy fix) and N8N_PROXY_HOPS (correct client IPs) are already set. If you run a handful of workflows on schedules and webhooks, this is the whole stack.
# n8n 2.x · SINGLE mode · sized for 300 runs/day (peak ≈10 concurrent)
# Generated by the n8n Server Sizing Calculator · topology follows n8n-io/n8n-hosting
name: n8n
services:
n8n:
image: docker.n8n.io/n8nio/n8n:stable
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678" # proxy handles the public side
env_file: .env
volumes:
- n8n_data:/home/node/.n8n
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" } # unrotated logs are the classic disk-killer
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=change-me
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 10s
retries: 5
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" } # unrotated logs are the classic disk-killer
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
volumes:
n8n_data:
pg_data:
caddy_data:
caddy_config:
# Production hardening: move task runners to a sidecar (external mode) —
# n8n docs → Hosting → Configuration → Task runners (image: n8nio/runners:stable)
The full queue-mode stack — main instance, Redis, workers, webhook processors — for when sustained parallel load outgrows one instance. You need it when concurrency, not speed, is the bottleneck: n8n's own benchmark took the same 4 GB hardware from 15 to 72 req/s by switching modes, but binary-heavy uploads actually got worse in queue mode and only ran clean at 32 GB — more RAM, not more workers, fixes those. Two traps are pre-handled in the file: N8N_CONCURRENCY_PRODUCTION_LIMIT overrides each worker's --concurrency in queue mode (3 workers at limit 10 = 30 concurrent), and DB_POSTGRESDB_POOL_SIZE is raised from its default of 2; OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS ships commented (the official template enables it) so manual test runs stay on the main instance where you can watch them.
# n8n 2.x · QUEUE mode · main + 2 workers + Redis + Postgres
# Generated by the n8n Server Sizing Calculator · topology follows n8n-io/n8n-hosting
name: n8n
services:
n8n-main:
image: docker.n8n.io/n8nio/n8n:stable
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678" # proxy handles the public side
env_file: .env
volumes:
- n8n_data:/home/node/.n8n
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" } # unrotated logs are the classic disk-killer
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
n8n-worker:
image: docker.n8n.io/n8nio/n8n:stable
restart: unless-stopped
command: worker --concurrency=10
env_file: .env
deploy:
replicas: 2
volumes:
- n8n_data:/home/node/.n8n
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" } # unrotated logs are the classic disk-killer
depends_on:
- n8n-main
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --maxmemory 128mb --maxmemory-policy noeviction
volumes:
- redis_data:/data
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" } # unrotated logs are the classic disk-killer
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=change-me
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 10s
retries: 5
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" } # unrotated logs are the classic disk-killer
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
logging:
driver: json-file
options: { max-size: "10m", max-file: "3" }
volumes:
n8n_data:
redis_data:
pg_data:
caddy_data:
caddy_config:
# Notes:
# · every service shares one .env → same N8N_ENCRYPTION_KEY (mandatory in queue mode)
# · binary data in 2.x queue mode lives in Postgres (filesystem unsupported; S3 = Enterprise license)
# · scale workers: docker compose up -d --scale n8n-worker=3
For AI-agent workflows, a single instance on an 8 GB box — the community-verified floor; 512 MB servers crash outright on AI Agent nodes. No GPU needed: model calls run on the provider's API, so RAM is the only spec that matters. This compose caps the Node heap with NODE_OPTIONS=--max-old-space-size plus N8N_RUNNERS_MAX_OLD_SPACE_SIZE at ~50-75% of RAM, so an over-hungry workflow fails predictably instead of taking the server down.
Topology matches example 1; the AI-specific part lives in .env — the heap and concurrency caps below are computed for an 8 GB box.
# ── n8n 2.x · generated by n8n Server Sizing Calculator ── # Sized for: 300 runs/day · peak concurrency ≈10 · SINGLE mode # ── network ── N8N_HOST=n8n.example.com WEBHOOK_URL=https://n8n.example.com/ N8N_PROTOCOL=https N8N_PROXY_HOPS=1 # trust one reverse proxy in front (X-Forwarded-For) GENERIC_TIMEZONE=UTC N8N_ENCRYPTION_KEY=change-me-openssl-rand-hex-24 # ── execution history (drives DB size — 4,200 runs ≈ 1 GB) ── EXECUTIONS_DATA_PRUNE=true EXECUTIONS_DATA_MAX_AGE=336 EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000 # High volume? Stop storing successful runs: # EXECUTIONS_DATA_SAVE_ON_SUCCESS=none # ── memory ── # Heap: Node's default old-space can lag your RAM — pin it to ~60% of the box NODE_OPTIONS=--max-old-space-size=4915 # 2.x: task runners are always on (N8N_RUNNERS_ENABLED is deprecated — don't set it). # For production, run runners as a sidecar (external mode) — see docs: hosting/configuration/task-runners N8N_RUNNERS_MAX_OLD_SPACE_SIZE=1229 # ── database ── DB_TYPE=postgresdb DB_POSTGRESDB_HOST=postgres DB_POSTGRESDB_DATABASE=n8n DB_POSTGRESDB_USER=n8n DB_POSTGRESDB_PASSWORD=change-me DB_POSTGRESDB_POOL_SIZE=12 # default is only 2 # ── single-mode guard against RAM storms ── N8N_CONCURRENCY_PRODUCTION_LIMIT=10
These three files cover the common cases — for your exact case, open the Step 2 builder and generate a compose and .env tuned to your executions per day, concurrency, and RAM.
Generate my compose →| Variable | What it does |
|---|---|
| EXECUTIONS_MODE | regular (default) or queue. Queue requires Redis plus at least one worker container; it multiplies concurrency, not per-run speed. |
| N8N_CONCURRENCY_PRODUCTION_LIMIT | Caps overlapping production executions. Trap: in queue mode it overrides each worker's --concurrency, so 3 workers at limit 10 = 30 concurrent. Worker default 10; n8n recommends 5 or higher. |
| DB_POSTGRESDB_POOL_SIZE | Postgres connections per instance. Default 2 — too low for queue mode; n8n docs say raise it there. |
| NODE_OPTIONS=--max-old-space-size | Node.js heap cap in MB. Set to ~50-75% of container RAM so memory failures are predictable instead of random OOM kills. |
| N8N_RUNNERS_MAX_OLD_SPACE_SIZE | The same heap cap for task runners — enabled by default in n8n 2.x (N8N_RUNNERS_ENABLED is deprecated), so without this the main-process cap is only half the story. |
| EXECUTIONS_DATA_MAX_AGE | Execution retention: pruned after 14 days by default (with EXECUTIONS_DATA_PRUNE=true). At ~0.25 MB per execution this is what keeps disk usage flat. |
| N8N_SECURE_COOKIE | The classic "can't log in behind a proxy" fix: when TLS terminates at the proxy and n8n sees plain HTTP, the auth cookie never sets. Don't blanket-disable it on a public box. |
| N8N_PROXY_HOPS | Number of reverse proxies in front of n8n. Must match the chain (Caddy alone = 1) or client IPs and rate limiting misbehave. |
| N8N_ENCRYPTION_KEY | Encrypts stored credentials. Pin it in .env and back it up — lose it and every saved credential is unrecoverable. |
| WEBHOOK_URL | Public base URL for webhook endpoints. Without it, n8n behind a proxy hands out internal URLs that external services can't reach. |
Yes — this page has three, prerendered from the same generator as our builder and matching the official n8n-io/n8n-hosting repo (1.7k stars): a minimal single-instance stack with Postgres and Caddy, a queue-mode stack with Redis and workers for sustained parallel load, and an AI-workflow single with heap caps for 8 GB servers. Copy them as-is.
Source: n8n-io/n8n-hosting (official repository)
Five groups cover most incidents: concurrency (N8N_CONCURRENCY_PRODUCTION_LIMIT — per n8n docs, in queue mode it overrides each worker's --concurrency, default 10, so effective total = limit x workers), database pooling (DB_POSTGRESDB_POOL_SIZE defaults to 2 — raise it for queue mode), heap caps (NODE_OPTIONS at 50-75% of RAM), proxy settings (N8N_SECURE_COOKIE breaks login when misset; N8N_PROXY_HOPS breaks client IPs), and 14-day execution pruning.
Source: docs.n8n.io (use-environment-variables reference; control-concurrency)
Barely. In n8n's own scalability benchmark, moving single-mode from 4 GB to 32 GB improved throughput only ~8%. Switching the same 4 GB machine to queue mode went from 15 req/s to 72 req/s at 0% failures. Hardware buys concurrency headroom and binary-file capacity, not execution speed — architecture does.
Source: blog.n8n.io — the n8n scalability benchmark
Budget memory, then cap concurrency. Community measurements put the idle Docker stack near 860 MB, and each overlapping light execution adds roughly 40-50 MB (heavy data work: 200-500 MB). Set N8N_CONCURRENCY_PRODUCTION_LIMIT to what your RAM covers, cap the Node heap, and move to queue mode when sustained parallel load outgrows one instance.
Source: Community memory measurements; n8n scalability benchmark
There is no best brand — sizing decides. Community measurements put the idle Docker stack near 860 MB plus 40-50 MB per overlapping light run, which makes roughly 2 GB a comfortable single-instance floor; AI-agent workflows need 8 GB (community-verified — 512 MB boxes crash on AI Agent nodes). A GPU adds nothing: LLM calls run on the provider's API.
Source: n8n community reports (memory measurements; AI Agent RAM floor)
Little, if you configure two things. Executions average ~0.25 MB on disk and n8n prunes them after 14 days by default (EXECUTIONS_DATA_MAX_AGE, per n8n docs) — 1,000 runs a day is roughly 3.5 GB in the retention window. The real disk-killer is unrotated Docker logs: set json-file max-size 10m, max-file 3 in your compose.
Source: docs.n8n.io (manage-execution-data); Docker logging documentation