Learn how to deploy n8n yourself in production: Docker Compose with PostgreSQL and Redis, Kubernetes via Helm, cloud VMs with a reverse proxy, choosing storage backends and persistent data, up to high availability and scaling worker nodes with queue mode.

In episode 14 you built governance for teams: roles, sharing, and audit. Now it's time for a permanent home. Throughout this series you've probably been running in desktop mode or a throwaway container — but production demands more: data must not be lost when a container restarts, the database must not become a point of failure, and load must be able to grow without a major overhaul.
Episode 15 covers Self-hosting & Deployment Patterns: deploying on Docker Compose, Kubernetes, and cloud VMs, choosing storage backends and persistent data, up to high availability and scaling worker nodes.
Before writing configuration files, decide on the stage first. Three common patterns:
The rule of thumb: start with Docker Compose on a single VM. Move up to Kubernetes only when your team or load genuinely needs orchestration — not because it's a trend.
A healthy production stack minimally consists of n8n, PostgreSQL as the database, and — if using queue mode — Redis as the message queue. Example of a complete composition:
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
n8n:
image: docker.n8n.io/n8nio/n8n
ports:
- "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_USER_MANAGEMENT_JWT_SECRET: ${N8N_USER_MANAGEMENT_JWT_SECRET}
WEBHOOK_URL: https://n8n.example.com/
GENERIC_TIMEZONE: Asia/Jakarta
TZ: Asia/Jakarta
volumes:
- n8n_data:/home/node/.n8n
depends_on:
- postgres
volumes:
postgres_data:
n8n_data:Note three things: all secrets (POSTGRES_PASSWORD, N8N_ENCRYPTION_KEY, N8N_USER_MANAGEMENT_JWT_SECRET) come from .env or a secret store, not hardcoded. The n8n_data and postgres_data volumes guarantee data survives container re-creation. And WEBHOOK_URL is set to the public domain so webhook payloads use the correct URL behind the proxy.
The database choice determines your instance's limits:
Besides the relational database, there's binary data (files downloaded or produced by workflows). By default n8n stores it in the filesystem within the n8n volume. For large-scale instances, storage mode can be directed to S3 with N8N_DEFAULT_BINARY_DATA_MODE=s3 along with its bucket credentials — moving file load from disk to object storage.
The key to all of it is persistence: every valuable state (database, binary data, .env) must live on restart-proof storage and be backed up periodically.
A cloud VM is essentially a Docker host at the end of the day. What distinguishes it from a laptop: incoming traffic must have TLS terminated by a reverse proxy. Caddy is a good choice because it automatically issues Let's Encrypt certificates:
n8n.example.com {
reverse_proxy n8n:5678
}Behind this proxy, WEBHOOK_URL in compose points to https://n8n.example.com/ so n8n builds correct webhook URLs. Also restrict port access in the security group: open only 80 and 443 to the public, and let 5678 be reachable only from the internal network.
Teams already using Kubernetes should use the official n8n Helm chart rather than writing manifests from scratch — the chart already handles Deployment, Service, Ingress, Secret, and persistence options:
helm repo add n8n https://n8n-io.github.io/helm-charts
helm repo update
helm install n8n n8n/n8n --values values.yamlCommon values set in values.yaml: image and tag, postgresql.enabled with a password from Secret, persistence for n8n_data, extraEnv for N8N_ENCRYPTION_KEY and N8N_USER_MANAGEMENT_JWT_SECRET, as well as ingress.host for the domain. In a cluster, the biggest benefits are that failed nodes are restarted automatically and volumes are carried by PersistentVolumeClaims.
When execution load starts to grow, executions don't always need to run in the main process. Queue mode moves executions to a Redis queue, and workers pull jobs from that queue. The main process still handles UI and API; workers consume the CPU for executions.
Enable it with EXECUTIONS_MODE=queue on all services, then run workers:
n8n worker --concurrency=10N8N_CONCURRENCY_PRODUCTION_LIMIT=10 n8n workerWorkers can run as separate containers with the same image — just the n8n worker command and the same env (EXECUTIONS_MODE=queue, QUEUE_BULL_REDIS_HOST, QUEUE_BULL_REDIS_PORT). Important note: concurrency below 5 can make the environment unstable, so start from that value upward. Don't forget to set N8N_COMMUNITY_NODES_ENABLED=true on both main and workers when using community nodes, because both sides must load the same nodes.
Setting up redundancy means eliminating single points of failure:
N8N_MULTI_MAIN_SETUP_ENABLED=true, allowing several main replicas behind a load balancer. The QUEUE_HEALTH_CHECK_ACTIVE=true health check and the /healthz endpoint from episode 13 ensure only healthy replicas serve traffic.pg_dump) and the binary data volume; test restores periodically, because a backup never tested is the same as no backup.Warning
Don't turn on queue mode and multi-main at the same time without a reason. Queue mode adds the Redis component; multi-main adds synchronization complexity. Add each layer only when the metrics in episode 13 genuinely show the need — avoid unused complexity.
Episode 15 moved n8n from experiment to production service. You chose a deployment strategy appropriate to context, built a Docker Compose stack with PostgreSQL and persistent volumes, secured access via a reverse proxy, managed deployments on Kubernetes with Helm, enabled queue mode for scaling workers, and set up high availability on top of Redis, PostgreSQL, and health checks.
Key takeaways:
.env that survive restarts.In the next episode we dig into performance: Performance Optimization — breaking large workflows into sub-workflows, batch processing and concurrency, as well as optimizing execution runtime and throughput. See you there!