Mastering n8n and Advanced Workflow Orchestration
Production Docker Deployment
Moving to Production
Running n8n locally is great for testing, but a production environment demands more. We need a setup that's reliable, secure, and can handle multiple workflows running at once. This means moving away from the default single-file database and putting a proper structure in place.
We'll use Docker Compose to define and run a multi-container application. This setup will consist of three main services: n8n itself, a robust database, and a reverse proxy to handle web traffic securely.
In this architecture, all incoming web traffic hits the reverse proxy first. It encrypts the connection using SSL/TLS and forwards valid requests to the n8n container. Instead of using SQLite, the n8n container connects to a separate PostgreSQL database container. This is crucial for production because PostgreSQL can handle multiple simultaneous connections from your workflows, preventing data corruption and improving performance.
Composing the Services
We'll define this entire setup in a single docker-compose.yml file. This file tells Docker exactly how to build and connect our services. We'll also use an .env file to store our configuration details, which keeps secrets out of the main configuration file.
It's a good practice to create a dedicated directory for your n8n setup, for example,
~/n8n-docker/. Place yourdocker-compose.ymland.envfiles inside it.
First, let's look at the database service. We define a PostgreSQL container, specify a volume to ensure our data persists even if the container is removed, and pass credentials through our environment file.
services:
postgres:
image: postgres:13
restart: always
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_NON_ROOT_USER=${POSTGRES_NON_ROOT_USER}
- POSTGRES_NON_ROOT_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
volumes:
- ./postgres-data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
interval: 10s
timeout: 5s
retries: 5
Next is the n8n service. This is where we connect to the database and set critical n8n-specific configurations.
n8n:
image: n8nio/n8n
restart: always
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
- N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- WEBHOOK_URL=${SUBDOMAIN}.${DOMAIN_NAME}
ports:
- "127.0.0.1:5678:5678"
volumes:
- ./n8n-data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
Two variables here are vital for security and functionality:
N8N_ENCRYPTION_KEY: This is a secret key used to encrypt all credentials you save in n8n (like API keys and database passwords). It must be a unique, random string of at least 32 characters. If you lose this key, you lose access to your credentials.WEBHOOK_URL: This is the public URL where your n8n instance can be reached. It's essential for trigger nodes like webhooks to work correctly, as external services need a way to send data back to n8n. It should look likehttps://n8n.yourdomain.com/.
Securing with a Reverse Proxy
Exposing n8n directly to the internet is not secure. A reverse proxy sits in front of n8n, handling all incoming traffic. It terminates the SSL/TLS connection, encrypting traffic between the user and your server, and then forwards the request to the n8n container over the internal Docker network.
Here, we'll add Traefik as our reverse proxy. It automatically discovers other containers and can generate SSL certificates from Let's Encrypt on the fly.
traefik:
image: traefik:v2.5
restart: always
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=${SSL_EMAIL}"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- ./letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
To make Traefik aware of n8n, we add a few labels to our n8n service definition. These labels tell Traefik to expose the service, what domain to use, and how to handle SSL.
n8n:
# ... (previous n8n config)
labels:
- traefik.enable=true
- traefik.http.routers.n8n.rule=Host(`${SUBDOMAIN}.${DOMAIN_NAME}`)
- traefik.http.routers.n8n.entrypoints=websecure
- traefik.http.routers.n8n.tls.certresolver=myresolver
- traefik.http.middlewares.n8n.headers.SSLProxyHeaders=X-Forwarded-Proto=https
- traefik.http.services.n8n.loadbalancer.server.port=5678
Self-Hosted vs. Cloud
This self-hosted setup gives you complete control over your data and infrastructure. It's ideal for organisations with strict data sovereignty requirements or those needing to integrate with internal systems that aren't exposed to the public internet.
However, this control comes with the responsibility of managing the infrastructure. You are in charge of server updates, security patches, backups, and monitoring. n8n Cloud abstracts all of this away, providing a managed platform where you can focus solely on building workflows. The trade-off is straightforward: DevOps overhead and cost in exchange for total data control, versus simplicity and speed with a managed service.
For any production deployment, n8n should be containerized using Docker.
Let's check your understanding of these production concepts.
What are the three main services defined in the recommended Docker Compose setup for a production n8n environment?
What is the primary purpose of the N8N_ENCRYPTION_KEY environment variable?
Deploying n8n to production using Docker Compose provides a powerful, scalable, and secure foundation for your automation.