n8n Self-Host
Install and operate self-hosted n8n responsibly. Most "n8n is broken in
production" tickets come from skipped Day-0 setup: wrong WEBHOOK_URL,
missing N8N_ENCRYPTION_KEY, default SQLite under load, no backup.
References: DOCKER.md | ENV_VARS.md | QUEUE_MODE.md | REVERSE_PROXY.md | SECURITY.md | BACKUP_UPGRADE.md
Lessons: LESSONS_LEARNED.md — read before non-trivial work.
Authoritative docs: https://docs.n8n.io/hosting/
Day-0 checklist (DO NOT SKIP)
Before activating any production workflow, these are non-negotiable:
Set N8N_ENCRYPTION_KEY to a random 32+ character string. Without it,
n8n generates one on first boot and writes it to disk — if that file is
lost, ALL credentials become unrecoverable.
openssl rand -hex 32
Set WEBHOOK_URL to the public URL where users reach the instance.
n8n reports webhook URLs based on this env. If it doesn't match what
callers hit, webhooks 404.
Set GENERIC_TIMEZONE to your team's timezone (e.g.
America/New_York). Schedule Trigger uses this as the default.
Use Postgres, not SQLite. SQLite is the default but doesn't survive
concurrent writes. Migrate before any real load.
Set up a reverse proxy with HTTPS. n8n serves plaintext by default.
Put nginx/Traefik/Caddy in front and terminate TLS. See REVERSE_PROXY.md.
Plan backups. At minimum: the Postgres database (or SQLite file) +
/home/node/.n8n config dir + binary data directory (if filesystem mode).
See BACKUP_UPGRADE.md.
Decide deployment mode early. If you'll need to scale beyond ~50
concurrent executions or have long-running workflows, plan for queue
mode from the start. See QUEUE_MODE.md.
Minimum viable Docker setup (single instance)
# docker-compose.yml
services:
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
ports:
- "5678:5678"
environment:
# Identity / encryption — DO NOT change after first boot
N8N_ENCRYPTION_KEY: "${N8N_ENCRYPTION_KEY}"
# Public URL
N8N_HOST: n8n.example.com
N8N_PROTOCOL: https
WEBHOOK_URL: https://n8n.example.com/
# Timezone
GENERIC_TIMEZONE: America/New_York
TZ: America/New_York
# DB — Postgres
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_PORT: 5432
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: "${POSTGRES_PASSWORD}"
# Reasonable defaults
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: "336" # 14 days in hours
N8N_METRICS: "true"
volumes:
- n8n_data:/home/node/.n8n
depends_on:
- postgres
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
n8n_data:
postgres_data:
# .env
N8N_ENCRYPTION_KEY=<openssl rand -hex 32 output>
POSTGRES_PASSWORD=<a long random password>
docker compose up -d
Then put nginx/Traefik/Caddy in front to terminate TLS for
n8n.example.com. See REVERSE_PROXY.md.
Queue mode — when and how
Switch to queue mode when ANY of these are true:
- More than ~50 concurrent active executions expected.
- Workflows that take >5 minutes (so the main process isn't tied up).
- Need to scale horizontally across machines.
- Need rolling restarts of workers without downtime.
Queue mode separates the main process (UI, webhooks, scheduler) from worker
processes (actual workflow execution), using Redis as a queue.
┌──────────────┐
Browser ─────▶│ n8n main │──┐
Webhook ─────▶│ (UI/API) │ │ enqueue
└──────────────┘ │
▼
┌────────┐
│ Redis │
└────────┘
▲
│ dequeue
┌─────────────────┴────────────────┐
│ │
┌──────────────┐ ┌──────────────┐
│ n8n worker │ ... │ n8n worker │
└──────────────┘ └──────────────┘
See QUEUE_MODE.md for the full Compose file
and tuning.
Critical env vars (always set in production)
| Env var |
Why |
N8N_ENCRYPTION_KEY |
Encrypts credentials. NEVER change after first boot |
WEBHOOK_URL |
Public URL n8n reports for webhooks |
N8N_HOST / N8N_PROTOCOL / N8N_PORT |
Internal URL components |
GENERIC_TIMEZONE / TZ |
Scheduler reference |
DB_TYPE=postgresdb + DB_POSTGRESDB_* |
Use Postgres |
EXECUTIONS_DATA_PRUNE=true |
Auto-clean old execution data |
EXECUTIONS_DATA_MAX_AGE=336 |
14 days (hours) of execution retention |
N8N_METRICS=true |
Prometheus metrics on /metrics |
EXECUTIONS_MODE=queue (queue mode) |
+ worker process + Redis |
QUEUE_BULL_REDIS_* (queue mode) |
Redis connection |
N8N_BLOCK_ENV_ACCESS_IN_NODE=true |
Stop expressions from reading host env (security) |
N8N_DIAGNOSTICS_ENABLED=false |
Disable telemetry to n8n.io (if you want) |
See ENV_VARS.md for the full catalog.
What breaks in production (most common)
| Symptom |
Cause |
Reference |
| All credentials disappear after restart |
N8N_ENCRYPTION_KEY regenerated |
SECURITY.md |
| Webhooks 404 |
WEBHOOK_URL doesn't match what callers hit |
REVERSE_PROXY.md |
| Schedule fires at wrong wall-clock time |
GENERIC_TIMEZONE wrong |
ENV_VARS.md |
| "Database is locked" errors |
Still on SQLite |
BACKUP_UPGRADE.md |
| Workers don't pick up jobs |
Queue mode env vars mismatched between main and workers |
QUEUE_MODE.md |
| Disk fills |
No execution data pruning |
ENV_VARS.md |
| OOM on large file processing |
Binary data not in filesystem/S3 mode |
SECURITY.md |
| Cannot enable SSO |
SSO is Enterprise-only |
https://docs.n8n.io/hosting/sso/ |
Procedure (any self-host request)
Identify what the user actually needs. "Install n8n" can mean
anything from docker run to a multi-node HA cluster. Ask:
- Cloud provider / on-prem?
- Expected concurrent executions and workflow duration?
- HTTPS termination already handled?
- Existing Postgres / Redis available?
- SSO required? (Enterprise)
Walk through Day-0 checklist before any specific setup. If the user
says "just give me Docker Compose", give them the Minimum viable Docker
setup above and DO call
out the checklist items they should set.
Use real values, not placeholders. When generating env vars, run
openssl rand -hex 32 and put the actual output in the user's
.env. Don't leave <YOUR_KEY_HERE> for them to forget about.
Generate a single docker-compose.yml + .env + reverse-proxy snippet
as a copy-paste-ready bundle. Don't make the user assemble pieces.
For upgrades, ALWAYS back up first. See BACKUP_UPGRADE.md.
Do not run docker compose pull && docker compose up -d without confirmation
if the user is on a major version boundary.
Update LESSONS_LEARNED.md if you discovered a
new hosting gotcha.
Best practices
Do:
- Pin the n8n image version (
n8nio/n8n:1.78.0) in production, not latest.
- Run as the non-root
node user (the official image already does).
- Mount
/home/node/.n8n as a named Docker volume or persistent disk —
this contains the SQLite DB (if using SQLite), the auto-generated
encryption key fallback, and some local config.
- Keep
N8N_ENCRYPTION_KEY in a secrets manager. Treat losing it as
equivalent to losing all credentials.
- Monitor
/healthz/readiness and /healthz for health checks.
- Scrape
/metrics with Prometheus for execution counts, durations,
queue depth.
- Run periodic
EXECUTIONS_DATA_PRUNE audits — if disk grows despite
pruning enabled, check EXECUTIONS_DATA_MAX_AGE setting.
- For Kubernetes, use the official Helm chart:
https://github.com/n8n-io/n8n-hosting/tree/main/kubernetes.
Don't:
- Don't run multiple n8n main processes against the same Postgres without
queue mode. Schedules will fire multiple times, webhooks may double-process.
- Don't use SQLite in production. Period.
- Don't expose port 5678 directly to the internet — always behind a proxy
with TLS.
- Don't rotate
N8N_ENCRYPTION_KEY casually — it requires re-encrypting
all stored credentials (n8n provides a CLI for this; do it deliberately).
- Don't skip backups because "Postgres has its own backup". You also need
/home/node/.n8n (config, license file, custom nodes if installed there)
and the binary data directory.
Continuous learning
Self-hosting touches a lot of moving parts (Docker, Postgres, Redis,
reverse proxy, your environment's specifics). Every non-trivial deployment
discovers something worth capturing. After any setup, debugging session,
or upgrade, append a dated entry to LESSONS_LEARNED.md.
1---2name: n8n-self-host3description: Install, configure, and operate self-hosted n8n. Use when user says "self-host n8n", "install n8n with Docker", "n8n docker-compose", "n8n on Kubernetes", "n8n behind nginx/Traefik/Caddy", "configure n8n env vars", "n8n queue mode", "scale n8n workers", "n8n with Postgres", "encryption key rotation", "back up n8n", "upgrade n8n version", "n8n SSO", "n8n SSL", "WEBHOOK_URL", "EXECUTIONS_MODE", "N8N_ENCRYPTION_KEY", "n8n external secrets", "n8n binary storage to S3". Covers Docker Compose deployments (single + queue mode), env vars catalog, queue mode + workers (Redis + multiple worker processes), reverse proxy patterns, Postgres setup and migration from SQLite, encryption key handling, backup strategies, version upgrades. Do NOT use for building workflows (use n8n-build-workflow), debugging workflow logic (use n8n-debug-workflow), or community-node packaging (use n8n-create-nodes).4license: MIT-05---67# n8n Self-Host89Install and operate self-hosted n8n responsibly. Most "n8n is broken in10production" tickets come from skipped Day-0 setup: wrong `WEBHOOK_URL`,11missing `N8N_ENCRYPTION_KEY`, default SQLite under load, no backup.1213**References:** [DOCKER.md](references/DOCKER.md) | [ENV_VARS.md](references/ENV_VARS.md) | [QUEUE_MODE.md](references/QUEUE_MODE.md) | [REVERSE_PROXY.md](references/REVERSE_PROXY.md) | [SECURITY.md](references/SECURITY.md) | [BACKUP_UPGRADE.md](references/BACKUP_UPGRADE.md)1415**Lessons:** [LESSONS_LEARNED.md](LESSONS_LEARNED.md) — read before non-trivial work.1617**Authoritative docs:** <https://docs.n8n.io/hosting/>1819---2021## Day-0 checklist (DO NOT SKIP)2223Before activating any production workflow, these are non-negotiable:24251. **Set `N8N_ENCRYPTION_KEY`** to a random 32+ character string. Without it,26 n8n generates one on first boot and writes it to disk — if that file is27 lost, ALL credentials become unrecoverable.2829 ```bash30 openssl rand -hex 3231 ```32332. **Set `WEBHOOK_URL`** to the public URL where users reach the instance.34 n8n reports webhook URLs based on this env. If it doesn't match what35 callers hit, webhooks 404.36373. **Set `GENERIC_TIMEZONE`** to your team's timezone (e.g.38 `America/New_York`). Schedule Trigger uses this as the default.39404. **Use Postgres, not SQLite.** SQLite is the default but doesn't survive41 concurrent writes. Migrate before any real load.42435. **Set up a reverse proxy with HTTPS.** n8n serves plaintext by default.44 Put nginx/Traefik/Caddy in front and terminate TLS. See [REVERSE_PROXY.md](references/REVERSE_PROXY.md).45466. **Plan backups.** At minimum: the Postgres database (or SQLite file) +47 `/home/node/.n8n` config dir + binary data directory (if filesystem mode).48 See [BACKUP_UPGRADE.md](references/BACKUP_UPGRADE.md).49507. **Decide deployment mode early.** If you'll need to scale beyond ~5051 concurrent executions or have long-running workflows, plan for **queue52 mode** from the start. See [QUEUE_MODE.md](references/QUEUE_MODE.md).5354---5556## Minimum viable Docker setup (single instance)5758```yaml59# docker-compose.yml60services:61 n8n:62 image: n8nio/n8n:latest63 restart: unless-stopped64 ports:65 - "5678:5678"66 environment:67 # Identity / encryption — DO NOT change after first boot68 N8N_ENCRYPTION_KEY: "${N8N_ENCRYPTION_KEY}"6970 # Public URL71 N8N_HOST: n8n.example.com72 N8N_PROTOCOL: https73 WEBHOOK_URL: https://n8n.example.com/7475 # Timezone76 GENERIC_TIMEZONE: America/New_York77 TZ: America/New_York7879 # DB — Postgres80 DB_TYPE: postgresdb81 DB_POSTGRESDB_HOST: postgres82 DB_POSTGRESDB_PORT: 543283 DB_POSTGRESDB_DATABASE: n8n84 DB_POSTGRESDB_USER: n8n85 DB_POSTGRESDB_PASSWORD: "${POSTGRES_PASSWORD}"8687 # Reasonable defaults88 EXECUTIONS_DATA_PRUNE: "true"89 EXECUTIONS_DATA_MAX_AGE: "336" # 14 days in hours90 N8N_METRICS: "true"91 volumes:92 - n8n_data:/home/node/.n8n93 depends_on:94 - postgres9596 postgres:97 image: postgres:16-alpine98 restart: unless-stopped99 environment:100 POSTGRES_USER: n8n101 POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"102 POSTGRES_DB: n8n103 volumes:104 - postgres_data:/var/lib/postgresql/data105106volumes:107 n8n_data:108 postgres_data:109```110111```bash112# .env113N8N_ENCRYPTION_KEY=<openssl rand -hex 32 output>114POSTGRES_PASSWORD=<a long random password>115```116117```bash118docker compose up -d119```120121Then put nginx/Traefik/Caddy in front to terminate TLS for122`n8n.example.com`. See [REVERSE_PROXY.md](references/REVERSE_PROXY.md).123124---125126## Queue mode — when and how127128Switch to queue mode when ANY of these are true:129130- More than ~50 concurrent active executions expected.131- Workflows that take >5 minutes (so the main process isn't tied up).132- Need to scale horizontally across machines.133- Need rolling restarts of workers without downtime.134135Queue mode separates the main process (UI, webhooks, scheduler) from worker136processes (actual workflow execution), using Redis as a queue.137138```139 ┌──────────────┐140 Browser ─────▶│ n8n main │──┐141 Webhook ─────▶│ (UI/API) │ │ enqueue142 └──────────────┘ │143 ▼144 ┌────────┐145 │ Redis │146 └────────┘147 ▲148 │ dequeue149 ┌─────────────────┴────────────────┐150 │ │151 ┌──────────────┐ ┌──────────────┐152 │ n8n worker │ ... │ n8n worker │153 └──────────────┘ └──────────────┘154```155156See [QUEUE_MODE.md](references/QUEUE_MODE.md) for the full Compose file157and tuning.158159---160161## Critical env vars (always set in production)162163| Env var | Why |164|---|---|165| `N8N_ENCRYPTION_KEY` | Encrypts credentials. NEVER change after first boot |166| `WEBHOOK_URL` | Public URL n8n reports for webhooks |167| `N8N_HOST` / `N8N_PROTOCOL` / `N8N_PORT` | Internal URL components |168| `GENERIC_TIMEZONE` / `TZ` | Scheduler reference |169| `DB_TYPE=postgresdb` + `DB_POSTGRESDB_*` | Use Postgres |170| `EXECUTIONS_DATA_PRUNE=true` | Auto-clean old execution data |171| `EXECUTIONS_DATA_MAX_AGE=336` | 14 days (hours) of execution retention |172| `N8N_METRICS=true` | Prometheus metrics on `/metrics` |173| `EXECUTIONS_MODE=queue` (queue mode) | + worker process + Redis |174| `QUEUE_BULL_REDIS_*` (queue mode) | Redis connection |175| `N8N_BLOCK_ENV_ACCESS_IN_NODE=true` | Stop expressions from reading host env (security) |176| `N8N_DIAGNOSTICS_ENABLED=false` | Disable telemetry to n8n.io (if you want) |177178See [ENV_VARS.md](references/ENV_VARS.md) for the full catalog.179180---181182## What breaks in production (most common)183184| Symptom | Cause | Reference |185|---|---|---|186| All credentials disappear after restart | `N8N_ENCRYPTION_KEY` regenerated | [SECURITY.md](references/SECURITY.md#encryption-key) |187| Webhooks 404 | `WEBHOOK_URL` doesn't match what callers hit | [REVERSE_PROXY.md](references/REVERSE_PROXY.md) |188| Schedule fires at wrong wall-clock time | `GENERIC_TIMEZONE` wrong | [ENV_VARS.md](references/ENV_VARS.md) |189| "Database is locked" errors | Still on SQLite | [BACKUP_UPGRADE.md](references/BACKUP_UPGRADE.md#migrate-sqlite-to-postgres) |190| Workers don't pick up jobs | Queue mode env vars mismatched between main and workers | [QUEUE_MODE.md](references/QUEUE_MODE.md) |191| Disk fills | No execution data pruning | [ENV_VARS.md](references/ENV_VARS.md#execution-data-retention) |192| OOM on large file processing | Binary data not in filesystem/S3 mode | [SECURITY.md](references/SECURITY.md#binary-data-storage) |193| Cannot enable SSO | SSO is Enterprise-only | <https://docs.n8n.io/hosting/sso/> |194195---196197## Procedure (any self-host request)1981991. **Identify what the user actually needs.** "Install n8n" can mean200 anything from `docker run` to a multi-node HA cluster. Ask:201 - Cloud provider / on-prem?202 - Expected concurrent executions and workflow duration?203 - HTTPS termination already handled?204 - Existing Postgres / Redis available?205 - SSO required? (Enterprise)2062072. **Walk through Day-0 checklist** before any specific setup. If the user208 says "just give me Docker Compose", give them the [Minimum viable Docker209 setup](#minimum-viable-docker-setup-single-instance) above and DO call210 out the checklist items they should set.2112123. **Use real values, not placeholders.** When generating env vars, run213 `openssl rand -hex 32` and put the actual output in the user's214 `.env`. Don't leave `<YOUR_KEY_HERE>` for them to forget about.2152164. **Generate a single `docker-compose.yml` + `.env` + reverse-proxy snippet**217 as a copy-paste-ready bundle. Don't make the user assemble pieces.2182195. **For upgrades, ALWAYS back up first.** See [BACKUP_UPGRADE.md](references/BACKUP_UPGRADE.md).220 Do not run `docker compose pull && docker compose up -d` without confirmation221 if the user is on a major version boundary.2222236. **Update [LESSONS_LEARNED.md](LESSONS_LEARNED.md)** if you discovered a224 new hosting gotcha.225226---227228## Best practices229230**Do:**231- Pin the n8n image version (`n8nio/n8n:1.78.0`) in production, not `latest`.232- Run as the non-root `node` user (the official image already does).233- Mount `/home/node/.n8n` as a named Docker volume or persistent disk —234 this contains the SQLite DB (if using SQLite), the auto-generated235 encryption key fallback, and some local config.236- Keep `N8N_ENCRYPTION_KEY` in a secrets manager. Treat losing it as237 equivalent to losing all credentials.238- Monitor `/healthz/readiness` and `/healthz` for health checks.239- Scrape `/metrics` with Prometheus for execution counts, durations,240 queue depth.241- Run periodic `EXECUTIONS_DATA_PRUNE` audits — if disk grows despite242 pruning enabled, check `EXECUTIONS_DATA_MAX_AGE` setting.243- For Kubernetes, use the official Helm chart:244 <https://github.com/n8n-io/n8n-hosting/tree/main/kubernetes>.245246**Don't:**247- Don't run multiple n8n main processes against the same Postgres without248 queue mode. Schedules will fire multiple times, webhooks may double-process.249- Don't use SQLite in production. Period.250- Don't expose port 5678 directly to the internet — always behind a proxy251 with TLS.252- Don't rotate `N8N_ENCRYPTION_KEY` casually — it requires re-encrypting253 all stored credentials (n8n provides a CLI for this; do it deliberately).254- Don't skip backups because "Postgres has its own backup". You also need255 `/home/node/.n8n` (config, license file, custom nodes if installed there)256 and the binary data directory.257258---259260## Continuous learning261262Self-hosting touches a lot of moving parts (Docker, Postgres, Redis,263reverse proxy, your environment's specifics). Every non-trivial deployment264discovers something worth capturing. After any setup, debugging session,265or upgrade, append a dated entry to [LESSONS_LEARNED.md](LESSONS_LEARNED.md).