Overview
Install, configure, scale, and operate self-hosted n8n instances. Targets n8n 2.x (stable 2.43 at the time of writing; 2.0 replaced "activate" with publish/unpublish and made task runners the default). Covers Docker Compose production setups, CLI administration, REST API automation, queue-mode scaling with Redis workers, backup/restore, monitoring, and troubleshooting. For building n8n workflows and configuring nodes, see the n8n-workflow skill.
Instructions
Task A: Production Docker Compose Setup
Basic setup with PostgreSQL (recommended over default SQLite):
# docker-compose.yml
services:
postgres:
image: postgres:16
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- db_storage:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: always
ports:
- "5678:5678"
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_ENCRYPTION_KEY=${ENCRYPTION_KEY}
- WEBHOOK_URL=${WEBHOOK_URL}
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE:-UTC}
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- N8N_PROXY_HOPS=1 # one reverse proxy in front
- N8N_RUNNERS_MODE=external # Code nodes run in the sidecar below
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
volumes:
- n8n_storage:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
task-runners:
image: n8nio/runners:${N8N_VERSION} # must match the n8n image version
restart: always
environment:
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
- N8N_RUNNERS_AUTH_TOKEN=${RUNNERS_AUTH_TOKEN}
depends_on:
- n8n
volumes:
db_storage:
n8n_storage:
Create a .env alongside with POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, ENCRYPTION_KEY (generate with openssl rand -hex 24), RUNNERS_AUTH_TOKEN (same way), N8N_VERSION (pin the current stable, e.g. 2.43.4, from the n8n releases page), WEBHOOK_URL, and GENERIC_TIMEZONE. Start with docker compose up -d.
Task runners isolate Code-node JavaScript and Python from n8n itself. Internal mode (the default child process) shares n8n's user and is deprecated; in production use external mode with the n8nio/runners sidecar as above. N8N_RUNNERS_ENABLED is deprecated since 2.0 and not needed. From n8n 2.30 N8N_WEBHOOK_URL is the preferred name of WEBHOOK_URL.
Task B: Scale with Queue Mode (Redis + Workers)
For high-volume workloads, add Redis and worker processes:
# Add to docker-compose.yml
x-n8n-shared: &n8n-shared
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: always
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true
- N8N_ENCRYPTION_KEY=${ENCRYPTION_KEY}
volumes:
- n8n_storage:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
services:
redis:
image: redis:7-alpine
restart: always
volumes:
- redis_storage:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
n8n:
<<: *n8n-shared
ports:
- "5678:5678"
n8n-worker:
<<: *n8n-shared
command: worker
# Scale with: docker compose up -d --scale n8n-worker=3
Each worker needs its own task-runner sidecar in external mode. Health endpoints on workers (/healthz, /healthz/readiness) exist only with QUEUE_HEALTH_CHECK_ACTIVE=true. Queue mode needs PostgreSQL; SQLite is not supported with workers. Optional webhook processors (command: webhook) scale incoming webhooks: route /webhook/* and /webhook-waiting/* to them and keep /webhook-test/* and the editor on the main instance.
Task C: CLI Administration
Run CLI commands inside Docker: docker exec -u node -it n8n n8n <command>.
# Export/import workflows (use a separate directory per entity type)
n8n export:workflow --backup --output=backups/workflows/
n8n export:workflow --id=Hk3p9WqLm2xYtRvB --published --output=workflow.json # published version
n8n import:workflow --input=workflow.json
n8n import:workflow --separate --input=backups/workflows/ # imported workflows come in unpublished
# Export/import credentials (--decrypted for cross-instance migration)
n8n export:credentials --backup --output=backups/credentials/
n8n export:credentials --all --decrypted --output=creds.json
n8n import:credentials --separate --input=backups/credentials/
# Full database backup/restore
n8n export:entities --outputDir=./backup --includeExecutionHistoryDataTables=true
n8n import:entities --inputDir=./backup --truncateTables=true
n8n execute --id=Hk3p9WqLm2xYtRvB # run a saved workflow
# Publish/unpublish workflows (2.0+; update:workflow --active is deprecated).
# These edit the database: restart n8n for them to take effect.
n8n publish:workflow --id=Hk3p9WqLm2xYtRvB
n8n unpublish:workflow --id=Hk3p9WqLm2xYtRvB
n8n unpublish:workflow --all # publish has no --all, by design
# User management
n8n user-management:reset # Reset to setup state (forgot password)
n8n mfa:disable --email=user@x.com # Disable MFA for locked-out user
n8n audit # security audit
--backup exports only workflows and credentials, not users, variables, execution history or the encryption key. A complete backup also needs the .n8n folder (it holds the key) and a PostgreSQL dump. --activeState=fromJson on import restores published state, but only in queue or multi-main mode.
Task D: REST API Automation
Create an API key in the n8n settings and send it in the X-N8N-API-KEY header. The self-hosted playground is at /api/v1/docs; the base path is /api/v1.
# List all workflows
curl -s -H "X-N8N-API-KEY: $N8N_API_KEY" http://localhost:5678/api/v1/workflows | jq '.data[].name'
# Publish a workflow (replaces /activate, deprecated)
curl -X POST -H "X-N8N-API-KEY: $N8N_API_KEY" http://localhost:5678/api/v1/workflows/Hk3p9WqLm2xYtRvB/publish
# Unpublish a workflow (replaces /deactivate, deprecated)
curl -X POST -H "X-N8N-API-KEY: $N8N_API_KEY" http://localhost:5678/api/v1/workflows/Hk3p9WqLm2xYtRvB/unpublish
# List failed executions
curl -s -H "X-N8N-API-KEY: $N8N_API_KEY" "http://localhost:5678/api/v1/executions?status=error" | jq '.data[] | {id, workflowId, status}'
Lists are paginated: pass the nextCursor value back as cursor (limit max 250).
Task E: Key Environment Variables
| Variable | Default | Purpose |
|---|---|---|
DB_TYPE | sqlite | sqlite or postgresdb |
N8N_ENCRYPTION_KEY | random | Credential encryption (preserve across restarts) |
WEBHOOK_URL | auto | Public URL for webhooks behind reverse proxy |
EXECUTIONS_MODE | regular | regular or queue (for scaling) |
EXECUTIONS_DATA_PRUNE | true | Auto-prune old execution data |
N8N_METRICS | false | Enable Prometheus /metrics endpoint |
N8N_RUNNERS_MODE | internal | external runs Code nodes in the n8nio/runners sidecar (recommended in production) |
GENERIC_TIMEZONE | America/New_York | Timezone for schedule triggers |
Use _FILE suffix for Docker Secrets (e.g., DB_POSTGRESDB_PASSWORD_FILE=/run/secrets/db_pass).
Examples
Example 1: Deploy n8n with PostgreSQL in production
User request: "Set up n8n in production with Docker Compose"
Actions taken:
$ mkdir n8n-production && cd n8n-production
$ openssl rand -hex 24 # use for ENCRYPTION_KEY and RUNNERS_AUTH_TOKEN
> 9f2c61b7d0e84a35c1f7a3e92b64d85077ac13e6f2b9d4c8
# Create .env with POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, ENCRYPTION_KEY,
# RUNNERS_AUTH_TOKEN, N8N_VERSION=2.43.4, WEBHOOK_URL, GENERIC_TIMEZONE
# Create docker-compose.yml (Task A template)
$ docker compose up -d
> [+] Running 4/4
> ✔ Network n8n-production_default Created
> ✔ Container n8n-production-postgres-1 Healthy
> ✔ Container n8n-production-n8n-1 Started
> ✔ Container n8n-production-task-runners-1 Started
$ docker compose logs n8n --tail 5
> n8n | Editor is now accessible via: http://localhost:5678
Example 2: Back up and restore workflows
User request: "Back up all my n8n workflows and credentials"
Actions taken:
$ docker exec -u node n8n n8n export:workflow --backup --output=/home/node/.n8n/backups/workflows/
> Successfully exported 12 workflows
$ docker exec -u node n8n n8n export:credentials --backup --output=/home/node/.n8n/backups/credentials/
> Successfully exported 8 credentials
$ docker cp n8n:/home/node/.n8n/backups/ ./n8n-backups/
# Restore on a new instance (finish the owner setup in the UI first)
$ docker cp ./n8n-backups/ n8n-new:/home/node/.n8n/backups/
$ docker exec -u node n8n-new n8n import:workflow --separate --input=/home/node/.n8n/backups/workflows/
> Successfully imported 12 workflows
$ docker exec -u node n8n-new n8n import:credentials --separate --input=/home/node/.n8n/backups/credentials/
The new instance needs the same N8N_ENCRYPTION_KEY, or export credentials with --decrypted and delete that file after the import. Imported workflows arrive unpublished; publish them in the UI.
Example 3: Recover from a failed update
User request: "My n8n instance is crashing after update"
Actions taken:
$ docker compose logs n8n --tail 30
> Error: Migration "AddWorkflowVersioning" failed ...
# Roll back: stop, pin the previous image tag in .env (N8N_VERSION=2.42.0),
# restore the PostgreSQL dump and the .n8n volume taken before the update
$ docker compose down
$ pg_restore --clean -d n8n backups/n8n-before-update.dump # run against the postgres container
$ docker compose up -d
> n8n | Editor is now accessible via: http://localhost:5678
Pinning an older image does not undo database migrations, so take a database dump and a copy of the .n8n volume before every upgrade.
Guidelines
- Always set
N8N_ENCRYPTION_KEYexplicitly and back it up; without it credentials cannot be decrypted on a new instance. - Use PostgreSQL for anything beyond a few workflows; queue mode requires it.
- Set
WEBHOOK_URLwhen behind a reverse proxy — webhooks fail silently without it. - Use
n8n export:credentials --decryptedwhen migrating between instances with different encryption keys, and delete the plain-text file afterwards. - Pin the image to a version (
2.43.4), notlatest; thestabletag is also fine for production but changes weekly. Keep then8nio/runnerstag equal to the n8n tag. - Enable execution pruning (
EXECUTIONS_DATA_PRUNE=true) to prevent database bloat. - In queue mode, all instances must share the same
N8N_ENCRYPTION_KEYand PostgreSQL database. - For health checks, use
/healthz(instance reachable) and/healthz/readiness(DB connected and migrated)./metricsneedsN8N_METRICS=true. - Code nodes without external task runners can reach credentials and the encryption key; do not let untrusted users edit workflows on such an instance.