⚙️Automation & Workflows

Deploy n8n with Docker Compose: Production-Ready Setup with PostgreSQL and Redis

Master production n8n deployment with Docker Compose, PostgreSQL, Redis queue management, multi-container architecture, security hardening, automated backups, monitoring, and enterprise scaling strategies.

Published January 20, 2025
15 min read
By Toolsana Team

After years of deploying n8n in production environments, I've learned that the difference between a hobby automation server and a mission-critical workflow engine often comes down to the initial architecture decisions you make. Too many teams start with a basic Docker run command and then struggle to scale when their automation needs grow exponentially. This guide walks through building a robust, production-ready n8n deployment using Docker Compose with PostgreSQL and Redis, covering everything from multi-container orchestration to enterprise-grade security configurations.

Understanding the production landscape

When you're running n8n in production, you're not just spinning up a container and calling it a day. You're building an automation infrastructure that needs to handle thousands of workflow executions, manage sensitive credentials securely, recover from failures gracefully, and scale with your organization's growing needs. The architecture we're building uses PostgreSQL as the primary database instead of SQLite, Redis for queue management and caching, and implements proper data persistence with automated backup strategies. Most importantly, we're configuring everything with security and scalability in mind from day one.

The transition from development to production reveals interesting performance characteristics. A single n8n instance can handle up to 220 workflow executions per second under optimal conditions, but most organizations hit performance walls around 5,000-10,000 daily executions without proper architecture. That's where queue mode with Redis becomes essential, allowing you to distribute workflow execution across multiple worker containers while maintaining a responsive UI and webhook processing layer.

Prerequisites and environment preparation

Before diving into the deployment, ensure your system meets the production requirements. You'll need a server with at least 4 CPU cores and 8GB of RAM for a basic production setup, though 16GB or more is recommended for high-volume environments. The host should be running Docker 20.10+ and Docker Compose 2.0+ for optimal compatibility with the latest n8n features. Storage-wise, allocate at least 20GB for the application and databases, preferably on SSD or NVMe drives for better performance.

Start by preparing your deployment directory structure. This organization helps maintain clarity as your deployment grows more complex over time:

sudo mkdir -p /opt/n8n/{config,data,postgres,redis,backups,scripts}
sudo chown -R 1000:1000 /opt/n8n/data
sudo chown -R 999:999 /opt/n8n/postgres
sudo chown -R 999:999 /opt/n8n/redis

The ownership settings are crucial here. The n8n container runs as user node with UID 1000, and getting these permissions wrong is one of the most common deployment issues I encounter. PostgreSQL and Redis typically run as UID 999, though this can vary depending on your base images.

Generate your encryption key before proceeding. This key encrypts all credentials stored in n8n and must remain consistent across all instances and upgrades:

openssl rand -hex 32 > /opt/n8n/config/encryption_key.txt
chmod 600 /opt/n8n/config/encryption_key.txt

Building the multi-container architecture

The heart of our production deployment is a carefully orchestrated multi-container setup. Rather than running everything in a single container, we separate concerns to improve scalability, maintainability, and fault tolerance. Create your main docker-compose.yml file:

version: '3.8'

volumes:
  postgres_data:
    driver: local
    driver_opts:
      type: none
      device: /opt/n8n/postgres
      o: bind
  n8n_data:
    driver: local
    driver_opts:
      type: none
      device: /opt/n8n/data
      o: bind
  redis_data:
    driver: local
    driver_opts:
      type: none
      device: /opt/n8n/redis
      o: bind

networks:
  n8n-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

x-shared: &shared
  restart: always
  image: docker.n8n.io/n8nio/n8n:latest
  environment:
    # Database Configuration
    - DB_TYPE=postgresdb
    - DB_POSTGRESDB_HOST=postgres
    - DB_POSTGRESDB_PORT=5432
    - DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}
    - DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER:-n8n}
    - DB_POSTGRESDB_PASSWORD_FILE=/run/secrets/postgres_password
    
    # Queue Configuration
    - EXECUTIONS_MODE=queue
    - QUEUE_BULL_REDIS_HOST=redis
    - QUEUE_BULL_REDIS_PORT=6379
    - QUEUE_HEALTH_CHECK_ACTIVE=true
    
    # Security and Encryption
    - N8N_ENCRYPTION_KEY_FILE=/run/secrets/encryption_key
    - N8N_SECURE_COOKIE=true
    - N8N_SAMESITE_COOKIE=strict
    
    # Production Settings
    - N8N_HOST=${DOMAIN:-n8n.example.com}
    - N8N_PORT=5678
    - N8N_PROTOCOL=https
    - WEBHOOK_URL=https://${DOMAIN:-n8n.example.com}/
    - N8N_PROXY_HOPS=1
    
    # Data Management
    - N8N_DEFAULT_BINARY_DATA_MODE=filesystem
    - EXECUTIONS_DATA_PRUNE=true
    - EXECUTIONS_DATA_PRUNE_MAX_COUNT=10000
    - EXECUTIONS_DATA_MAX_AGE=168
    
    # Security Hardening
    - N8N_BLOCK_ENV_ACCESS_IN_NODE=true
    - N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES=true
    - N8N_RESTRICT_FILE_ACCESS_TO=/data/files:/tmp
    
    # Performance
    - NODE_OPTIONS=--max-old-space-size=4096
    
  volumes:
    - n8n_data:/home/node/.n8n
    - ./files:/data/files
  networks:
    - n8n-network
  secrets:
    - encryption_key
    - postgres_password
  depends_on:
    redis:
      condition: service_healthy
    postgres:
      condition: service_healthy

services:
  # Main n8n Instance - Handles UI, API, and triggers
  n8n:
    <<: *shared
    container_name: n8n-main
    ports:
      - "127.0.0.1:5678:5678"
    command: ["n8n"]
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.n8n.rule=Host(`${DOMAIN:-n8n.example.com}`)"
      - "traefik.http.routers.n8n.entrypoints=websecure"
      - "traefik.http.routers.n8n.tls.certresolver=letsencrypt"
      - "traefik.http.services.n8n.loadbalancer.server.port=5678"

  # Worker Instances - Execute workflows
  n8n-worker:
    <<: *shared
    command: ["worker", "--concurrency=10"]
    deploy:
      replicas: 2
    environment:
      - N8N_CONCURRENCY_PRODUCTION_LIMIT=10
      - N8N_GRACEFUL_SHUTDOWN_TIMEOUT=300

  # PostgreSQL Database
  postgres:
    image: postgres:16-alpine
    restart: always
    container_name: n8n-postgres
    environment:
      - POSTGRES_USER=${POSTGRES_USER:-postgres}
      - POSTGRES_PASSWORD_FILE=/run/secrets/postgres_root_password
      - POSTGRES_DB=${POSTGRES_DB:-n8n}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro
    networks:
      - n8n-network
    secrets:
      - postgres_root_password
      - postgres_password
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -h localhost -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-n8n}']
      interval: 5s
      timeout: 5s
      retries: 10
    command: >
      postgres
      -c shared_buffers=256MB
      -c effective_cache_size=1GB
      -c maintenance_work_mem=64MB
      -c max_connections=200

  # Redis for Queue Management
  redis:
    image: redis:7-alpine
    restart: always
    container_name: n8n-redis
    volumes:
      - redis_data:/data
      - ./config/redis.conf:/usr/local/etc/redis/redis.conf:ro
    networks:
      - n8n-network
    healthcheck:
      test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
      interval: 5s
      timeout: 5s
      retries: 5
    command: redis-server /usr/local/etc/redis/redis.conf

  # Traefik Reverse Proxy
  traefik:
    image: traefik:v3.0
    restart: always
    container_name: traefik
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL:-admin@example.com}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - n8n-network
    security_opt:
      - no-new-privileges:true

secrets:
  encryption_key:
    file: ./config/encryption_key.txt
  postgres_root_password:
    file: ./config/postgres_root_password.txt
  postgres_password:
    file: ./config/postgres_password.txt

This configuration might look complex at first glance, but each section serves a specific purpose. The x-shared block defines common configuration that both the main instance and workers inherit, reducing duplication and ensuring consistency. The main n8n service handles the web interface, API endpoints, and trigger processing, while worker instances execute the actual workflows in isolation.

Environment variables and secrets management

Security in production isn't optional, and proper secrets management forms the foundation of a secure deployment. Create your environment configuration file:

sudo nano /opt/n8n/.env
# Domain Configuration
DOMAIN=n8n.yourdomain.com
ACME_EMAIL=admin@yourdomain.com

# Database Configuration
POSTGRES_USER=postgres
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=n8n

# Basic Authentication (Optional but recommended)
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_HASH=$2a$10$... # Generate with: htpasswd -bnBC 10 "" password | tr -d ':'

# Timezone
GENERIC_TIMEZONE=America/New_York
TZ=America/New_York

Now generate secure passwords for your services. Never use default passwords in production:

# Generate secure passwords
echo "$(openssl rand -base64 32)" > /opt/n8n/config/postgres_root_password.txt
echo "$(openssl rand -base64 32)" > /opt/n8n/config/postgres_password.txt
chmod 600 /opt/n8n/config/*.txt

The beauty of using Docker secrets with the _FILE suffix is that sensitive values never appear in environment variables or process listings. This approach prevents accidental exposure through logs or debugging tools. When n8n sees an environment variable ending in _FILE, it reads the actual value from the specified file at runtime.

Create the database initialization script to set up the non-root user with appropriate permissions:

-- /opt/n8n/scripts/init-db.sql
CREATE USER n8n WITH ENCRYPTED PASSWORD 'will_be_replaced_by_secret';
GRANT ALL PRIVILEGES ON DATABASE n8n TO n8n;
ALTER DATABASE n8n OWNER TO n8n;

-- Performance optimizations
ALTER SYSTEM SET shared_buffers = '256MB';
ALTER SYSTEM SET effective_cache_size = '1GB';
ALTER SYSTEM SET max_connections = 200;
ALTER SYSTEM SET checkpoint_completion_target = 0.9;
SELECT pg_reload_conf();

Configuring Redis for queue management

Redis serves as the message broker for queue mode, enabling horizontal scaling of workflow execution. Configure Redis for production use:

sudo nano /opt/n8n/config/redis.conf
# Memory management
maxmemory 1gb
maxmemory-policy allkeys-lru

# Persistence for recovery
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec

# Performance tuning
tcp-keepalive 60
timeout 0
tcp-backlog 511
databases 2

# Security
requirepass your_redis_password_here

The persistence settings ensure Redis can recover queue data after unexpected restarts. The maxmemory-policy of allkeys-lru allows Redis to evict least recently used keys when memory limits are reached, preventing memory exhaustion while maintaining recent queue data.

Data persistence and automated backups

Data persistence in n8n involves multiple layers: the PostgreSQL database storing workflow definitions and metadata, the filesystem storing binary data and execution logs, and Redis maintaining queue state. Implementing comprehensive backup strategies for each layer ensures you can recover from any failure scenario.

Create an automated backup script that handles all components:

sudo nano /opt/n8n/scripts/backup.sh
#!/bin/bash
set -euo pipefail

# Configuration
BACKUP_DIR="/opt/n8n/backups"
RETENTION_DAYS=30
DB_CONTAINER="n8n-postgres"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

echo "[$(date)] Starting n8n backup..."

# Create timestamped backup directory
CURRENT_BACKUP="$BACKUP_DIR/$TIMESTAMP"
mkdir -p "$CURRENT_BACKUP"

# Backup PostgreSQL database
echo "[$(date)] Backing up PostgreSQL database..."
docker exec "$DB_CONTAINER" pg_dump -U n8n -d n8n -Fc > "$CURRENT_BACKUP/database.dump"

# Backup n8n data directory (workflows, credentials, settings)
echo "[$(date)] Backing up n8n data..."
tar -czf "$CURRENT_BACKUP/n8n_data.tar.gz" -C /opt/n8n/data .

# Export workflows and credentials via n8n CLI
echo "[$(date)] Exporting workflows and credentials..."
docker exec n8n-main n8n export:workflow --backup --output=/tmp/workflows_backup.json
docker exec n8n-main n8n export:credentials --backup --output=/tmp/credentials_backup.json
docker cp n8n-main:/tmp/workflows_backup.json "$CURRENT_BACKUP/"
docker cp n8n-main:/tmp/credentials_backup.json "$CURRENT_BACKUP/"

# Create backup manifest
cat > "$CURRENT_BACKUP/manifest.json" <<EOF
{
  "timestamp": "$TIMESTAMP",
  "date": "$(date -Iseconds)",
  "version": "$(docker exec n8n-main n8n --version)",
  "components": ["database", "data", "workflows", "credentials"]
}
EOF

# Compress entire backup
tar -czf "$BACKUP_DIR/n8n_backup_$TIMESTAMP.tar.gz" -C "$BACKUP_DIR" "$TIMESTAMP"
rm -rf "$CURRENT_BACKUP"

# Cleanup old backups
echo "[$(date)] Cleaning up old backups..."
find "$BACKUP_DIR" -name "n8n_backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete

# Optional: Upload to S3
if [ -n "${S3_BACKUP_BUCKET:-}" ]; then
    echo "[$(date)] Uploading to S3..."
    aws s3 cp "$BACKUP_DIR/n8n_backup_$TIMESTAMP.tar.gz" \
        "s3://$S3_BACKUP_BUCKET/n8n/backups/" \
        --storage-class STANDARD_IA
fi

echo "[$(date)] Backup completed successfully"

Make the script executable and schedule it with cron:

sudo chmod +x /opt/n8n/scripts/backup.sh

# Add to crontab for daily 2 AM backups
sudo crontab -e
0 2 * * * /opt/n8n/scripts/backup.sh >> /opt/n8n/backups/backup.log 2>&1

The restore process is equally important. Create a restoration script for disaster recovery:

sudo nano /opt/n8n/scripts/restore.sh
#!/bin/bash
set -euo pipefail

BACKUP_FILE="$1"
RESTORE_DIR="/tmp/restore_$$"

if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup_file.tar.gz>"
    exit 1
fi

echo "[$(date)] Starting restoration from $BACKUP_FILE..."

# Stop services
docker-compose down

# Extract backup
mkdir -p "$RESTORE_DIR"
tar -xzf "$BACKUP_FILE" -C "$RESTORE_DIR"
BACKUP_NAME=$(ls "$RESTORE_DIR")

# Restore PostgreSQL database
echo "[$(date)] Restoring database..."
docker-compose up -d postgres
sleep 10
docker exec -i n8n-postgres psql -U postgres -c "DROP DATABASE IF EXISTS n8n;"
docker exec -i n8n-postgres psql -U postgres -c "CREATE DATABASE n8n OWNER n8n;"
docker exec -i n8n-postgres pg_restore -U n8n -d n8n < "$RESTORE_DIR/$BACKUP_NAME/database.dump"

# Restore n8n data
echo "[$(date)] Restoring n8n data..."
rm -rf /opt/n8n/data/*
tar -xzf "$RESTORE_DIR/$BACKUP_NAME/n8n_data.tar.gz" -C /opt/n8n/data

# Start all services
docker-compose up -d

echo "[$(date)] Restoration completed"
rm -rf "$RESTORE_DIR"

Performance optimization and scaling strategies

Performance tuning in n8n requires understanding your workflow patterns and adjusting configurations accordingly. The default settings work well for light usage, but production environments need optimization based on actual workload characteristics.

For CPU-intensive workflows involving data transformation or complex logic, lower worker concurrency prevents resource contention. Update your worker configuration:

n8n-worker-cpu:
  <<: *shared
  command: ["worker", "--concurrency=5"]
  environment:
    - N8N_CONCURRENCY_PRODUCTION_LIMIT=5
    - NODE_OPTIONS=--max-old-space-size=8192
  deploy:
    replicas: 4

For I/O-bound workflows making many external API calls, higher concurrency maximizes throughput:

n8n-worker-io:
  <<: *shared
  command: ["worker", "--concurrency=20"]
  environment:
    - N8N_CONCURRENCY_PRODUCTION_LIMIT=20
    - NODE_OPTIONS=--max-old-space-size=2048
  deploy:
    replicas: 2

Implement horizontal auto-scaling based on queue depth. Create a monitoring script that adjusts worker count:

sudo nano /opt/n8n/scripts/autoscale.sh
#!/bin/bash

MIN_WORKERS=2
MAX_WORKERS=10
SCALE_UP_THRESHOLD=50
SCALE_DOWN_THRESHOLD=10

# Get current queue depth
QUEUE_DEPTH=$(docker exec n8n-redis redis-cli LLEN bull:n8n:wait)
CURRENT_WORKERS=$(docker-compose ps n8n-worker | grep -c "Up")

if [ "$QUEUE_DEPTH" -gt "$SCALE_UP_THRESHOLD" ] && [ "$CURRENT_WORKERS" -lt "$MAX_WORKERS" ]; then
    NEW_WORKERS=$((CURRENT_WORKERS + 1))
    echo "[$(date)] Scaling up to $NEW_WORKERS workers (queue depth: $QUEUE_DEPTH)"
    docker-compose up -d --scale n8n-worker=$NEW_WORKERS
elif [ "$QUEUE_DEPTH" -lt "$SCALE_DOWN_THRESHOLD" ] && [ "$CURRENT_WORKERS" -gt "$MIN_WORKERS" ]; then
    NEW_WORKERS=$((CURRENT_WORKERS - 1))
    echo "[$(date)] Scaling down to $NEW_WORKERS workers (queue depth: $QUEUE_DEPTH)"
    docker-compose up -d --scale n8n-worker=$NEW_WORKERS
fi

Monitoring and health checks

Production deployments require comprehensive monitoring to detect issues before they impact users. Configure Prometheus metrics collection by adding monitoring services to your docker-compose:

# Add to docker-compose.yml services section
prometheus:
  image: prom/prometheus:latest
  container_name: prometheus
  volumes:
    - ./config/prometheus.yml:/etc/prometheus/prometheus.yml:ro
    - prometheus_data:/prometheus
  command:
    - '--config.file=/etc/prometheus/prometheus.yml'
    - '--storage.tsdb.path=/prometheus'
    - '--web.console.libraries=/usr/share/prometheus/console_libraries'
    - '--web.console.templates=/usr/share/prometheus/consoles'
  networks:
    - n8n-network
  ports:
    - "127.0.0.1:9090:9090"

grafana:
  image: grafana/grafana:latest
  container_name: grafana
  volumes:
    - grafana_data:/var/lib/grafana
    - ./config/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
    - ./config/grafana/datasources:/etc/grafana/provisioning/datasources:ro
  environment:
    - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin}
    - GF_INSTALL_PLUGINS=redis-datasource
  networks:
    - n8n-network
  ports:
    - "127.0.0.1:3000:3000"

Create the Prometheus configuration:

# /opt/n8n/config/prometheus.yml
global:
  scrape_interval: 30s
  evaluation_interval: 30s

scrape_configs:
  - job_name: 'n8n'
    static_configs:
      - targets: ['n8n:5678']
    metrics_path: /metrics

  - job_name: 'postgres'
    static_configs:
      - targets: ['postgres-exporter:9187']

  - job_name: 'redis'
    static_configs:
      - targets: ['redis-exporter:9121']

Configure health check endpoints for container orchestration:

# Health check configuration in docker-compose
healthcheck:
  test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:5678/healthz']
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

Set up alerting rules for critical metrics. Create an alerts configuration:

# /opt/n8n/config/prometheus/alerts.yml
groups:
  - name: n8n_alerts
    interval: 30s
    rules:
      - alert: HighErrorRate
        expr: rate(n8n_execution_failed_total[5m]) > 0.1
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "High error rate detected"
          description: "Error rate is {{ $value }} errors per second"

      - alert: QueueBacklog
        expr: n8n_queue_bull_queue_waiting > 100
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Large queue backlog"
          description: "{{ $value }} workflows waiting in queue"

      - alert: HighMemoryUsage
        expr: n8n_process_resident_memory_bytes > 3221225472
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High memory usage"
          description: "Process using {{ $value | humanize }} of memory"

      - alert: DatabaseConnectionFailure
        expr: up{job="postgres"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "PostgreSQL is down"
          description: "Cannot connect to PostgreSQL database"

Troubleshooting common deployment issues

Even with careful planning, production deployments encounter issues. The most common problem involves permissions, particularly when n8n cannot write to its data directory. The symptom appears as EACCES: permission denied, open '/home/node/.n8n/config' in the logs. The solution involves ensuring the data directory has the correct ownership before starting containers:

# Fix permission issues
docker-compose down
sudo chown -R 1000:1000 /opt/n8n/data
docker-compose up -d

Database connection issues often manifest as workflows failing to execute or the UI becoming unresponsive. Check database connectivity from within the n8n container:

# Test database connection
docker exec -it n8n-main sh -c 'apt-get update && apt-get install -y postgresql-client'
docker exec -it n8n-main psql -h postgres -U n8n -d n8n -c "SELECT version();"

Memory issues become apparent when handling large datasets or running many concurrent workflows. The Node.js heap exhaustion error FATAL ERROR: Ineffective mark-compacts near heap limit indicates insufficient memory allocation. Increase the heap size through the NODE_OPTIONS environment variable, but remember that container limits must also accommodate the increase:

# Update memory limits in docker-compose.yml
environment:
  - NODE_OPTIONS=--max-old-space-size=8192
deploy:
  resources:
    limits:
      memory: 10G

Webhook issues frequently arise from incorrect URL configuration or reverse proxy misconfigurations. Ensure your WEBHOOK_URL environment variable matches your public domain and includes the protocol. For reverse proxy setups, WebSocket support is crucial for real-time updates:

# Nginx configuration for WebSocket support
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_cache_bypass $http_upgrade;
}

When workflows hang or execute slowly, examine the execution data pruning settings. Without proper pruning, the execution history table grows unbounded, degrading performance:

# Check execution table size
docker exec n8n-postgres psql -U n8n -d n8n -c "
SELECT 
  schemaname,
  tablename,
  pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables 
WHERE tablename LIKE '%execution%'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;"

Advanced security configurations

Security in production extends beyond basic authentication. Implementing defense-in-depth strategies protects your automation infrastructure from various attack vectors. Start with network isolation using Docker networks to prevent unauthorized container communication. The configuration we've built uses a dedicated bridge network, but you can further enhance security with network policies.

Configure fail2ban to protect against brute force attacks:

sudo nano /etc/fail2ban/jail.d/n8n.conf
[n8n-auth]
enabled = true
port = https
filter = n8n-auth
logpath = /opt/n8n/data/logs/n8n.log
maxretry = 5
findtime = 600
bantime = 3600

Implement rate limiting at the reverse proxy level to prevent API abuse:

# Nginx rate limiting configuration
http {
    limit_req_zone $binary_remote_addr zone=n8n_api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=n8n_webhook:10m rate=50r/s;
    
    server {
        location /api/ {
            limit_req zone=n8n_api burst=20 nodelay;
            proxy_pass http://127.0.0.1:5678;
        }
        
        location /webhook/ {
            limit_req zone=n8n_webhook burst=100 nodelay;
            proxy_pass http://127.0.0.1:5678;
        }
    }
}

Enable audit logging to track all configuration changes and sensitive operations. Configure n8n with enhanced logging:

{
  "log": {
    "level": "info",
    "output": ["console", "file"],
    "file": {
      "location": "/home/node/.n8n/logs/n8n.log",
      "maxsize": 100,
      "maxcount": 30
    }
  },
  "audit": {
    "enabled": true,
    "events": ["workflow.create", "workflow.update", "workflow.delete", "credentials.create", "credentials.update", "credentials.delete"]
  }
}

Conclusion: Building for the long term

Deploying n8n in production with Docker Compose provides a robust foundation for automation infrastructure that can grow with your organization. The architecture we've built separates concerns properly, with PostgreSQL handling persistent data, Redis managing queue operations, and multiple worker instances ensuring scalability. The security configurations protect sensitive workflow data while the monitoring setup provides visibility into system health and performance bottlenecks.

The key to successful production deployment lies in starting with the right architecture rather than trying to retrofit production requirements onto a development setup. By implementing proper secrets management, automated backups, and comprehensive monitoring from the beginning, you avoid the technical debt that often accumulates when systems grow organically. The queue mode configuration with Redis enables horizontal scaling that can handle anything from hundreds to millions of workflow executions, simply by adjusting the worker count.

Remember that this setup is a starting point that you'll refine based on your specific workload patterns. Monitor your metrics, analyze performance bottlenecks, and adjust configurations accordingly. Some workflows benefit from higher concurrency, others from more memory, and understanding these patterns helps you optimize resource utilization. The investment in proper infrastructure pays dividends through improved reliability, easier maintenance, and the confidence that your automation platform can handle whatever demands your organization places on it.

Share this post: