Self-Hosted Production Deployment
Chapter 18: Self-Hosted Production Deployment
Moving from a development setup to production is a significant step. This chapter provides a battle-tested deployment blueprint: Docker Compose with PostgreSQL and Redis, Nginx reverse proxy with SSL, environment-based secrets management, automated backup scripts, zero-downtime upgrade procedures, and multi-user permission configuration.
18.1 Architecture Overview
The recommended production stack has four components: n8n (workflow engine and UI), PostgreSQL 16 (replacing SQLite for production-grade concurrency), Redis 7 (message queue for Queue mode, covered in Ch20), and Nginx (reverse proxy, SSL termination, WebSocket support).
Why replace SQLite? SQLite has poor concurrent write performance. When multiple workflows execute simultaneously, SQLite's locking contention causes execution failures. PostgreSQL supports true concurrency and has mature backup tooling โ it's the only correct choice for production.
18.2 Docker Compose Configuration
# docker-compose.yml (production)
version: '3.8'
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- n8n_net
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- n8n_net
n8n:
image: n8nio/n8n:1.45.0
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file: .env
volumes:
- n8n_data:/home/node/.n8n
ports:
- "127.0.0.1:5678:5678"
networks:
- n8n_net
volumes:
postgres_data:
redis_data:
n8n_data:
networks:
n8n_net:
Port binding: Bind n8n to
127.0.0.1:5678, not0.0.0.0:5678. This ensures n8n only accepts connections from the local Nginx proxy โ external traffic cannot reach port 5678 directly.
18.3 Environment Variable Security
All secrets go into a .env file that is git-ignored. Never hardcode passwords in the compose file.
# .env template
POSTGRES_DB=n8n_prod
POSTGRES_USER=n8n_user
POSTGRES_PASSWORD=# openssl rand -base64 32
REDIS_PASSWORD=# openssl rand -base64 24
# Encryption key for all saved credentials
# Generate: openssl rand -hex 32
# WARNING: never change this after initial setup
N8N_ENCRYPTION_KEY=your-32-char-hex-string
N8N_HOST=n8n.yourdomain.com
N8N_PROTOCOL=https
WEBHOOK_URL=https://n8n.yourdomain.com/
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
DB_POSTGRESDB_USER=${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
EXECUTIONS_DATA_MAX_AGE=30
EXECUTIONS_DATA_PRUNE=true
18.4 Nginx + SSL Configuration
# /etc/nginx/sites-available/n8n.conf
server {
listen 80;
server_name n8n.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name n8n.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/n8n.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/n8n.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 64M;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
# WebSocket support (required for n8n execution logs)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
}
}
# Obtain and auto-renew Let's Encrypt SSL certificate
apt install certbot python3-certbot-nginx -y
certbot --nginx -d n8n.yourdomain.com
certbot renew --dry-run
18.5 Backup Strategy
Back up both PostgreSQL (workflow data) and the n8n data volume (encrypted credentials). Run the backup script via cron at 3 AM daily, retain 30 days of backups.
#!/bin/bash
# crontab: 0 3 * * * /opt/n8n/backup.sh
BACKUP_DIR=/opt/n8n/backups
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# Backup PostgreSQL
docker exec n8n-postgres-1 pg_dump -U $POSTGRES_USER $POSTGRES_DB \
| gzip > $BACKUP_DIR/pg_$TIMESTAMP.sql.gz
# Backup n8n data volume (credentials and config)
docker run --rm \
-v n8n_n8n_data:/data \
-v $BACKUP_DIR:/backup \
alpine tar czf /backup/n8n_data_$TIMESTAMP.tar.gz -C /data .
# Delete backups older than 30 days
find $BACKUP_DIR -name "*.gz" -mtime +30 -delete
18.6 Zero-Downtime Upgrade Procedure
- Run a full backup before any upgrade
- Review the GitHub release notes for breaking changes
docker compose pull n8nโ pull the new imagedocker compose up -d n8nโ replace the container (brief ~20s downtime during DB migration)docker compose logs -f n8nโ verify migration completed cleanly- Run a test workflow to confirm everything works
Pin your version: Never use the
latesttag in production. Use a specific version liken8nio/n8n:1.45.0and update the version number deliberately during planned upgrade windows.
18.7 User and Permission Management
n8n has supported multiple users since v0.219. Recommended production settings:
- Disable open registration: use
N8N_USER_MANAGEMENT_DISABLED=false(default) and invite members via the admin UI - Role hierarchy: Owner (full settings access) โ Admin (user management) โ Member (own workflows only)
- LDAP/SAML: enterprise feature for SSO integration with Azure AD, Okta, etc.
# Key security environment variables
# Prevent workflow-owner bypass
N8N_WORKFLOW_OWNER_BLOCK=true
# Block importing workflows from external URLs (supply chain protection)
N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES=true
# Limit execution data retention to avoid unbounded disk growth
EXECUTIONS_DATA_MAX_AGE=30
EXECUTIONS_DATA_PRUNE=true