Home · About · Services · Industries · Projects · Products · Process · Blog · FAQ · Contact
Self-hosting n8n in production: Postgres, queue mode and backups How to run n8n reliably on your own server: PostgreSQL, the encryption key, HTTPS and webhooks, queue mode with Redis workers, pruning, upgrades and backups.
A typical story: someone starts n8n with a one-line docker run on a spare VPS, builds a dozen useful workflows, and six months later the business depends on them. Then the container is recreated during an upgrade, the SQLite file was never on a volume, and every workflow and credential is gone. Or the data survived but the encryption key did not, so every saved credential is unreadable.
Self-hosting n8n in production means treating it like any other stateful application. In short: use PostgreSQL instead of SQLite, set and back up a fixed N8N_ENCRYPTION_KEY, put it behind a reverse proxy with HTTPS and a correct WEBHOOK_URL, pin the image version, prune execution data, and move to queue mode with Redis and workers once load grows.
This post covers each of those in the order we set them up, with a Docker Compose file you can adapt. It assumes you already know what n8n is and want it to stay up. If you are deciding what to automate in the first place, our post on n8n content pipelines shows a real workflow end to end.
Before you start: licence and sizing n8n's Community Edition is free to self-host under the Sustainable Use License. That covers running it for your own organisation's internal workflows. It does not cover reselling n8n itself as a hosted service to others, and some features such as SSO, log streaming, external secrets and Git-based environments sit in paid plans. Read the licence and the feature table before you commit, and ask a lawyer if your use case is borderline.
For sizing, a small team's workload (a few hundred executions a day, mostly API calls) runs comfortably on a 2 vCPU, 4 GB VPS with PostgreSQL on the same host. Memory, not CPU, is usually what runs out first, because n8n holds each execution's data in memory while it runs. Workflows that pull large files or thousands of rows in one go are the ones to watch.
A production Docker Compose baseline This is the shape we start from on a single server. It runs n8n in regular mode against PostgreSQL, with all state on named volumes and the version pinned.
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
interval: 10s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_HOST: n8n.example.com
N8N_PROTOCOL: https
WEBHOOK_URL: https://n8n.example.com/
N8N_PROXY_HOPS: 1
GENERIC_TIMEZONE: Asia/Kolkata
TZ: Asia/Kolkata
EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: 168
EXECUTIONS_DATA_PRUNE_MAX_COUNT: 50000
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
volumes:
pg_data:
n8n_data:Keep the secrets in a .env file next to the Compose file, readable only by root, and never in the repository. Generate the key once with openssl rand -hex 32. A few of these settings deserve a closer look:
N8N_VERSION should be an exact tag such as a specific 2.x release, never latest. An unplanned major upgrade on a container restart is how many self-hosted instances break. Binding the port to 127.0.0.1 means only the reverse proxy on the same host can reach n8n. Nothing on port 5678 is exposed to the internet. WEBHOOK_URL is the public address n8n uses when it shows webhook URLs and OAuth callback URLs. If it is wrong, webhooks appear as http://localhost:5678 and OAuth sign-ins fail. GENERIC_TIMEZONE sets the default timezone for Schedule Trigger nodes. Without it, a job meant for 9am local time runs at 9am UTC. EXECUTIONS_DATA_MAX_AGE is in hours, so 168 keeps a week of history. Execution data is the main reason n8n databases grow, so decide how much history you actually need. The encryption key is the thing you cannot lose n8n encrypts every saved credential (API keys, OAuth tokens, database passwords) with N8N_ENCRYPTION_KEY. If you do not set one, n8n generates a key on first start and writes it to the config file in /home/node/.n8n. That works until the volume is lost or you move to a new server and bring only the database. The workflows come across, but every credential fails to decrypt.
Set the key explicitly from day one and store a copy in your password manager or secrets store, separate from the server. In queue mode, every main, worker and webhook process must use the same key. If you are taking over an instance where the key was auto-generated, read it out of the config file now and pin it in the environment.
# read the auto-generated key from an existing instance
docker compose exec n8n cat /home/node/.n8n/configReverse proxy, HTTPS and access n8n should sit behind a reverse proxy that terminates TLS. NGINX, Traefik and Caddy all work. The two details that trip people up are WebSockets, which the editor uses for live execution updates, and request size, since webhooks that receive files can be larger than the proxy default.
server {
listen 443 ssl;
http2 on;
server_name n8n.example.com;
ssl_certificate /etc/letsencrypt/live/n8n.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/privkey.pem;
client_max_body_size 50m;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 300s;
}
}N8N_PROXY_HOPS: 1 in the Compose file tells n8n to trust the X-Forwarded-* headers from one proxy, so it sees the real client IP and protocol. The http2 on directive needs NGINX 1.25.1 or later; on older versions put http2 on the listen line instead.
Anyone who logs in to the editor can call any API your credentials allow. We restrict it rather than relying on the login page alone. Common options are putting the editor behind a VPN or Tailscale and exposing only /webhook/ paths publicly, or adding an IP allow-list for everything except webhooks. Ask every user to turn on two-factor authentication either way, and review which nodes you allow. Execute Command and similar nodes that touch the host can be blocked with NODES_EXCLUDE, and n8n 2.x already blocks Execute Command and Local File Trigger by default.
When to switch to queue mode In the default regular mode, one process serves the editor, receives webhooks and runs every execution. That is fine until a heavy workflow makes the editor sluggish or webhook responses slow down. Queue mode splits the work: the main process handles the UI, triggers and webhooks and pushes execution IDs to Redis, and one or more worker processes pull jobs and run them.
# Move the n8n environment into a shared anchor at the top of the file
x-n8n-env: &n8n-env
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
EXECUTIONS_MODE: queue
QUEUE_BULL_REDIS_HOST: redis
QUEUE_HEALTH_CHECK_ACTIVE: "true"
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: "true"
# ...plus the host, webhook, timezone and pruning settings from above
services:
redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- redis_data:/data
n8n:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
environment: *n8n-env
# ports, volumes, depends_on as before
n8n-worker:
image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
command: worker --concurrency=10
restart: unless-stopped
environment: *n8n-env
depends_on: [postgres, redis]
# add redis_data to the volumes listWorkers need the same database settings, encryption key and n8n version as the main process. The concurrency flag sets how many executions each worker runs at once; 10 is the default and a sensible start. Scale by adding workers, not by raising concurrency until a single worker runs out of memory. With QUEUE_HEALTH_CHECK_ACTIVE set, each worker answers on /healthz so your orchestrator can restart a stuck one.
Binary data needs a decision in queue mode. The filesystem mode stores files on the local disk of whichever process ran the step, so it does not work across separate workers, and n8n's built-in S3 storage mode is a licensed feature. Check the binary data docs for your version before you switch. For workflows that move large files, upload them to object storage such as S3 or MinIO early in the workflow and pass the object key between nodes instead of the file itself.
Backups, upgrades and monitoring The PostgreSQL database holds everything that matters: workflows, credentials, users and execution history. Back it up nightly with pg_dump to off-site storage, and back up the .env file (or at least the encryption key) separately. We also export workflows as JSON, which gives readable diffs and an easy way to restore a single workflow someone broke.
# nightly database dump
docker compose exec -T postgres pg_dump -U n8n -Fc n8n > n8n-$(date +%F).dump
# workflows as separate JSON files, suitable for committing to Git
docker compose exec n8n n8n export:workflow --backup --output=/home/node/.n8n/backups/Do not export credentials with --decrypted into Git. Restore the database dump into a scratch PostgreSQL container every month or so and start n8n against it with the saved key. That is the only way to know the backup and the key actually go together.
For upgrades, read the release notes for every version you skip, especially across major versions, which have changed defaults such as Code node environment access and task runners. Take a database dump, change N8N_VERSION, and restart. n8n runs its database migrations on start-up, so watch the logs for the first boot. In queue mode, stop the workers, upgrade the main process first, then bring workers back on the same version.
For monitoring, an external uptime check on /healthz catches the instance being down. It does not catch workflows that run and fail. For those, build a small workflow that starts with an Error Trigger node and posts to Slack, Teams or email, then select it under Workflow settings, Error workflow, on every production workflow. If you already run Prometheus, set N8N_METRICS=true to expose /metrics, and add N8N_METRICS_INCLUDE_QUEUE_METRICS=true in queue mode to track queue depth.
Production checklist PostgreSQL, not SQLite, with data on a named volume. N8N_ENCRYPTION_KEY set explicitly and stored outside the server. Image pinned to an exact version; upgrades planned and tested on staging. HTTPS reverse proxy with WebSocket headers, correct WEBHOOK_URL and N8N_PROXY_HOPS. Editor restricted by VPN or allow-list, two-factor on for every user. Execution pruning configured to match the history you need. Nightly database dumps off-site, workflow JSON exports, and a tested restore. An error workflow wired to a channel someone reads, plus an external uptime check. Queue mode with Redis and workers once heavy workflows slow the editor or webhooks. If you already have workflows that the business relies on and none of this is in place, start with the key and the database backup. Those two are what turn a lost server from a disaster into an afternoon's work.
Infigini IT Services · 11 Kanwali Road, Dehradun 248001, India · info@infigini.com · +91 95684 87789