Before you start
n8n can run with an internal SQLite file, but for anything serious you should use PostgreSQL — it's more robust with many workflows and concurrent runs. Prerequisite: a working Docker installation.
Compose stack
PostgreSQLA complete stack with n8n + Postgres on an isolated network. Put it in a folder, e.g. ~/n8n/.
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pg_data:/var/lib/postgresql/data
networks: [ internal ]
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER}" ]
interval: 10s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
DB_POSTGRESDB_USER: ${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_HOST: ${N8N_HOST}
N8N_PORT: 5678
N8N_PROTOCOL: https
WEBHOOK_URL: https://${N8N_HOST}/
GENERIC_TIMEZONE: Europe/Copenhagen
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
ports:
- "127.0.0.1:5678:5678"
volumes:
- n8n_data:/home/node/.n8n
networks: [ internal, web ]
volumes:
pg_data:
n8n_data:
networks:
internal:
internal: true
web:
Configuration (.env)
Keep secrets in a .env file next to compose.yaml — never in the YAML itself.
POSTGRES_USER=n8n
POSTGRES_PASSWORD=# generate: openssl rand -base64 24
POSTGRES_DB=n8n
N8N_HOST=n8n.defencia.dk
N8N_ENCRYPTION_KEY=# generate: openssl rand -hex 32
N8N_ENCRYPTION_KEY is used to encrypt all stored credentials. If it isn't set explicitly, it's auto-generated in the volume — and if you lose the volume without having the key, all credentials are irrecoverably lost. Store the key in Bitwarden alongside your other secrets.| Important env variable | Function |
|---|---|
N8N_ENCRYPTION_KEY | Encrypts stored credentials (save it!) |
WEBHOOK_URL | Public base URL n8n builds webhook addresses from |
N8N_HOST / N8N_PROTOCOL | Hostname and protocol behind the proxy |
GENERIC_TIMEZONE | Time zone for cron/schedule nodes |
N8N_SECURE_COOKIE | Set true behind HTTPS (default) |
EXECUTIONS_DATA_PRUNE | Auto-clear old execution logs |
N8N_RUNNERS_ENABLED | Enable task runners (isolated code execution) |
Reverse proxy (Nginx)
n8n uses websockets for the live editor — the proxy block must handle upgrade headers.
server {
listen 443 ssl;
server_name n8n.defencia.dk;
# ssl_certificate ... (Certbot inserts)
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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
}
}
sudo certbot --nginx -d n8n.defencia.dk — or use your existing wildcard for *.defencia.dk.Scaling — queue mode
advancedFor many concurrent or heavy workflows, n8n can run in queue mode: a main instance handles the UI/webhooks, and separate worker containers run the jobs via Redis.
redis service, set EXECUTIONS_MODE=queue and QUEUE_BULL_REDIS_HOST=redis on both main and workers, and start n workers with the command n8n worker. Each worker pulls jobs from the queue — scale by increasing the number of worker containers.Operation
docker compose up -d
docker compose logs -f n8n
docker compose pull
docker compose up -d
docker compose exec postgres \
pg_dump -U n8n n8n > n8n_$(date +%F).sql
docker compose exec n8n \
n8n export:workflow --all --output=/home/node/.n8n/backup.json
pg_dump (data + credentials, encrypted with the encryption key) and a workflow export (portable JSON). Send both to restic/pCloud. Remember: without the encryption key, the credentials in the dump are useless.Hardening
| Control | Recommendation |
|---|---|
| Exposure | Bind to 127.0.0.1, only via Nginx + HTTPS |
| Database | Postgres on the internal network, no host port |
| Encryption key | Set explicitly, store in Bitwarden, back up separately from the volume |
| Login | Enable user management/2FA; Fail2ban jail on the login endpoint |
| Webhooks | Use webhook auth/tokens; validate payloads in the workflow |
| Task runners | Enable to isolate Code-node execution |
| Execution data | Prune old logs so they don't accumulate sensitive data |
| Access | Consider VPN-only access to the editor; webhooks separately |
127.0.0.1:5678 — not 0.0.0.0 — otherwise the port can be exposed to the internet past UFW. See the Docker guide for details.