Back · Hosting
self-hosted

Self-hosted (Docker)

Full control with a single container.

Difficulty
Monthly cost from: ~$5–20/mo (VPS)
Best for: Devs who want ownership, custom nodes, and no per-execution fees.

Run n8n yourself with Docker Compose on any VPS or bare-metal box. Full data ownership, unlimited executions, and complete control of the stack.

Pros

  • Full data ownership
  • Unlimited executions
  • Install community & custom nodes freely
  • Cheap for high-volume workflows

Cons

  • You own upgrades, backups, TLS, monitoring
  • Needs Docker + reverse-proxy know-how
  • Queue mode requires Redis + workers for scale

Prerequisites

  • A VPS with Ubuntu 22.04+ (min 2 vCPU / 2GB RAM)
  • A domain name pointed to the server's IP
  • Docker Engine + Docker Compose plugin
  • Basic command-line comfort

Step-by-step guide

  1. Step 1

    Provision the server

    Spin up a VPS (Hetzner, DO, Linode…), point your domain's A record to its IP.

    ssh root@your-server-ip
  2. Step 2

    Install Docker

    Use the official convenience script.

    curl -fsSL https://get.docker.com | sh
  3. Step 3

    Write docker-compose.yml

    Define Postgres, n8n, and Caddy services with named volumes.

    services:
      postgres:
        image: postgres:16
        restart: unless-stopped
        environment:
          POSTGRES_USER: n8n
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
          POSTGRES_DB: n8n
        volumes: [ pgdata:/var/lib/postgresql/data ]
    
      n8n:
        image: n8nio/n8n:latest
        restart: unless-stopped
        depends_on: [ postgres ]
        ports: [ "5678:5678" ]
        environment:
          DB_TYPE: postgresdb
          DB_POSTGRESDB_HOST: postgres
          DB_POSTGRESDB_DATABASE: n8n
          DB_POSTGRESDB_USER: n8n
          DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
          N8N_HOST: ${N8N_HOST}
          N8N_PROTOCOL: https
          N8N_PORT: 5678
          WEBHOOK_URL: https://${N8N_HOST}/
          N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
          GENERIC_TIMEZONE: Europe/Berlin
        volumes: [ n8n_data:/home/node/.n8n ]
    
      caddy:
        image: caddy:2
        restart: unless-stopped
        ports: [ "80:80", "443:443" ]
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile
          - caddy_data:/data
          - caddy_config:/config
    
    volumes: { pgdata: {}, n8n_data: {}, caddy_data: {}, caddy_config: {} }
  4. Step 4

    Configure the Caddyfile

    Point your domain at the n8n container; Caddy issues Let's Encrypt certs automatically.

    your-domain.com {
      reverse_proxy n8n:5678
    }
  5. Step 5

    Set environment variables & launch

    Create a .env with POSTGRES_PASSWORD, N8N_HOST, N8N_ENCRYPTION_KEY, then start the stack.

    docker compose up -d

Scaling & queue mode

For higher throughput, switch to queue mode: set EXECUTIONS_MODE=queue, add a Redis instance, and run one or more separate `n8n worker` containers alongside the main process. The main instance handles the editor UI and webhooks while workers pull jobs from Redis. Scale worker replicas horizontally as load grows, and use a managed Postgres (with read replicas if needed) once execution volume is high.

Backup & upgrade

Back up the Postgres database (pg_dump on a cron schedule, or your provider's managed snapshot feature) and the n8n_data volume (contains binary data & config). Before upgrading, snapshot both, read the release notes for breaking changes, pull the new image tag, then `docker compose up -d`. Roll back by restoring the previous image tag + database snapshot if anything fails.

Security checklist

  • Always run behind HTTPS (Caddy/Traefik/Nginx + Let's Encrypt) — never expose port 5678 directly.
  • Set a strong, permanent N8N_ENCRYPTION_KEY and back it up (losing it breaks stored credentials).
  • Use Postgres, not the default SQLite, for anything beyond local testing.
  • Enable N8N_BASIC_AUTH or an SSO/proxy layer if the instance is reachable from the internet.
  • Restrict inbound firewall rules to 80/443 (and SSH from your IP only).
  • Keep the n8n image pinned and update on a schedule instead of always using :latest in production.

Cost breakdown

  • Small VPS (2GB RAM)$5–12/mo
  • Domain name~$10/yr
  • TLS certs (Let's Encrypt)Free

Troubleshooting & FAQ

+Can I use SQLite instead of Postgres?

n8n defaults to SQLite, which works for testing but isn't recommended for production: no concurrent writers, harder backups, and risk of corruption under load. Use Postgres for anything real.

+My webhooks return 404 / don't trigger — why?

WEBHOOK_URL must match the public HTTPS URL exactly (including trailing slash), and the workflow must be activated (not just saved) for production webhook URLs to work.

+How do I move from Self-hosted Docker to another host later?

Export workflows/credentials with the n8n CLI (`n8n export:workflow --all` / `export:credentials --all`) or the REST API, restore the Postgres dump on the new host, then import.

+Do I lose data if the container restarts?

No, as long as n8n_data and the Postgres volume are mounted as named Docker volumes (not ephemeral container storage), your data persists across restarts and image updates.