n8n Workflow Automation: Self-Host a Powerful Zapier Alternative in Docker

Complete guide to self-hosting n8n using Docker Compose. Connect apps, build advanced workflows, and automate tasks with a self-hosted integration hub.

This guide provides a production-ready blueprint for deploying self-hosted n8n on a Virtual Private Server (VPS) using Docker Compose.

Prerequisites and System Requirements

Before beginning the deployment, ensure your VPS meets the following minimum requirements: * OS: Linux (Ubuntu 22.04 LTS or Debian 12 recommended) * Specs: Minimum 1 vCPU and 1 GB RAM (2 vCPUs and 4 GB RAM recommended for production workloads with high concurrency) * Software: Docker Engine (v24.0+) and Docker Compose (v2.0+) installed * Network: A fully qualified domain name (FQDN) with A records pointing to your VPS public IPv4 address

Database Selection: SQLite vs. PostgreSQL

n8n supports two database backends for storing workflow data, credentials, and execution logs:

  1. SQLite (Default): A file-based database that requires zero configuration. Ideal for testing, development, or low-volume setups. It is not recommended for production because concurrent write operations can lead to database locking (SQLITE_BUSY errors).
  2. PostgreSQL: A robust relational database management system. Required for production environments, multi-user setups, high-volume executions, and queue-mode scaling.

In this guide, we configure a dedicated PostgreSQL container alongside n8n to ensure stability under heavy workloads.

The Production Docker Compose Stack

Create a directory named n8n-docker on your VPS and place the following configuration files inside it.

1. docker-compose.yml

This manifest defines three services: the n8n application engine, the PostgreSQL database, and a Caddy reverse proxy to handle automatic SSL/TLS certificate provisioning and renewal.

version: '3.8'

networks:
  n8n-network:
    driver: bridge

volumes:
  caddy_data:
  caddy_config:
  postgres_data:
  n8n_data:

services:
  postgres:
    image: postgres:16-alpine
    container_name: n8n-postgres
    restart: always
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - n8n-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5

  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    container_name: n8n-app
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "127.0.0.1:5678:5678"
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
      - DB_POSTGRESDB_USER=${POSTGRES_USER}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - N8N_HOST=${N8N_SUBDOMAIN}.${N8N_DOMAIN}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://${N8N_SUBDOMAIN}.${N8N_DOMAIN}/
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
      # Security Configurations
      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
      # Performance Optimization
      - EXECUTIONS_PROCESS=main
      - EXECUTIONS_DATA_PRUNE=true
      - EXECUTIONS_DATA_MAX_AGE=168 # Keep execution logs for 7 days
      - EXECUTIONS_DATA_PRUNE_TIMEOUT=3600
    volumes:
      - n8n_data:/home/node/.n8n
    networks:
      - n8n-network

  caddy:
    image: caddy:2-alpine
    container_name: n8n-caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - n8n
    networks:
      - n8n-network

2. .env Environment Configuration

This file externalizes configuration secrets and parameters. Ensure you populate the placeholders with secure credentials.

# Domain Configuration
N8N_DOMAIN=example.com
N8N_SUBDOMAIN=n8n
GENERIC_TIMEZONE=UTC

# Database Configuration
POSTGRES_USER=n8n_db_user
POSTGRES_DB=n8n_database
# Generate a strong password (e.g., openssl rand -base64 24)
POSTGRES_PASSWORD=bkBmDbkhgpo7S8mFceQfsdcq

# n8n Security
# CRITICAL: Generate a 32-character hex key (openssl rand -hex 24)
N8N_ENCRYPTION_KEY=your_generated_hex_key_here

3. Caddy Reverse Proxy Configuration (Caddyfile)

Caddy acts as a reverse proxy, automatically terminating SSL and forwarding traffic to n8n. Create a file named Caddyfile in the same directory:

{$N8N_SUBDOMAIN}.{$N8N_DOMAIN} {
    reverse_proxy n8n:5678 {
        header_up Host {upstream_hostport}
        header_up X-Real-IP {remote_host}
    }
}

Alternative Reverse Proxy: Nginx Configuration

If you prefer Nginx over Caddy, you can expose n8n on port 5678 locally and proxy traffic using Nginx. Below is a production-hardened Nginx server block configuration supporting WebSockets (required for n8n execution screens).

server {
    listen 80;
    server_name n8n.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name n8n.example.com;

    ssl_certificate /etc/letsencrypt/live/n8n.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/privkey.pem;

    # Modern SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_set_header Connection '';
        proxy_http_version 1.1;
        proxy_chunked_transfer_encoding off;
        proxy_buffering off;
        proxy_cache off;

        # WebSocket support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Header propagation
        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;
    }
}

Hardening and Security Configuration

A self-hosted integration hub is a high-value target for attackers. Configure these security practices to protect your instance:

1. Generating the Encryption Key

The N8N_ENCRYPTION_KEY is used to encrypt credential data (API tokens, passwords) before saving it to the database. If this key is lost, you will not be able to decrypt your stored credentials. Generate a secure, random string using:

openssl rand -hex 24

Store this key securely (e.g., in a password manager) outside of your VPS.

2. Disabling Public Registrations

Once the owner account is created, disable public signups to prevent unauthorized users from creating accounts on your n8n instance. Add the following environment variable to the n8n service in your docker-compose.yml:

      - N8N_PERSONALIZATION_ENABLED=false
      - N8N_USER_MANAGEMENT_DISABLED=false

3. Restricting Execution Data Retention

To prevent the PostgreSQL database from ballooning in size and slow down performance, limit the log retention period:

      - EXECUTIONS_DATA_PRUNE=true
      - EXECUTIONS_DATA_MAX_AGE=168 # Retain execution data for 7 days (168 hours)

Deployment Steps

Execute the following commands on your VPS to start the containers:

  1. Create the application directory and files: bash mkdir -p ~/n8n-docker && cd ~/n8n-docker
  2. Populate docker-compose.yml, .env, and Caddyfile with your configurations.
  3. Validate and launch the stack in detached mode: bash docker compose up -d
  4. Verify the services are running and check logs: bash docker compose ps docker compose logs -f n8n

You can now access your self-hosted n8n instance at https://n8n.example.com and proceed with the initial owner account creation wizard.


Upgrading Self-Hosted n8n

To safely upgrade n8n to the latest version, run the following commands sequentially:

cd ~/n8n-docker
# Pull the latest Docker images
docker compose pull
# Recreate containers with the new image version
docker compose up -d
# Prune old images to free disk space
docker image prune -f