Chapter 18

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, not 0.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

  1. Run a full backup before any upgrade
  2. Review the GitHub release notes for breaking changes
  3. docker compose pull n8n — pull the new image
  4. docker compose up -d n8n — replace the container (brief ~20s downtime during DB migration)
  5. docker compose logs -f n8n — verify migration completed cleanly
  6. Run a test workflow to confirm everything works

Pin your version: Never use the latest tag in production. Use a specific version like n8nio/n8n:1.45.0 and 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:

# 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
Rate this chapter
4.7  / 5  (11 ratings)

💬 Comments