DEV Community

Cover image for Self-Hosting n8n in 2026: A Production Setup That Doesn't Bite You in Week Two
TrackStack
TrackStack

Posted on • Originally published at trackstack.tech

Self-Hosting n8n in 2026: A Production Setup That Doesn't Bite You in Week Two

— Self-hosting n8n in 2026 costs $4–12/month for the VPS and gives you unlimited workflow executions plus full data control. The setup is Docker Compose with PostgreSQL behind an Nginx reverse proxy with Let's Encrypt SSL — about 30 minutes if you've used a Linux server before. This is the production config I actually run, plus the gotchas that bite people in week two.

I've set up self-hosted n8n for myself and a handful of clients over the past year. Every time, I went looking for "the one good guide" and ended up stitching together six tutorials, two GitHub issues, and one Reddit thread. Here's the consolidated version, biased toward the choices that survive contact with real production traffic.

Why bother self-hosting at all

n8n Cloud starts at €24/month for 2,500 executions. A €4 Hetzner VPS gives you unlimited executions and complete data control. The math becomes obvious fast — but cost isn't the only reason. Self-hosting wins when:

  • You exceed 50,000 executions/month. Above this, every hosted automation platform becomes painful. n8n self-hosted has zero per-execution cost.
  • Data residency matters. Healthcare, fintech, GDPR-strict EU scenarios — controlling the infra is the cleanest path to compliance.
  • You need custom code or npm packages. Self-hosted lets you write JS or Python and install packages hosted platforms restrict.
  • You want platform independence. No vendor lockout, no surprise pricing changes.

The trade-off is real: you own uptime, backups, security patches, SSL renewal. If that sounds painful, look at managed n8n hosting instead — it's $7–25/month and someone else handles the boring parts. For broader options, here's a comparison of Zapier alternatives including n8n.

Picking a VPS

Minimum: 1 vCPU, 1 GB RAM, 25 GB SSD — fine for personal use, enable swap to avoid OOM kills.
Production: 2 vCPU, 4 GB RAM, 40+ GB SSD. Sweet spot for 90% of self-hosters.

Provider Plan Specs Price Notes
Hetzner CAX11 (ARM) 2 vCPU / 4 GB / 40 GB €3.29/mo Best price/perf in EU
DigitalOcean Basic Droplet 2 vCPU / 4 GB / 80 GB $24/mo Polished UX, $200 sign-up credit
Vultr High Performance 2 vCPU / 4 GB / 80 GB $24/mo 32 datacenters globally
Contabo VPS S 4 vCPU / 8 GB / 200 GB ~$7/mo Most RAM per dollar

Hetzner CAX11 is what I run for everything that doesn't need US latency. The €3.29/month for 2 vCPU + 4 GB ARM is genuinely unbeatable — only friction is the identity verification on signup that takes a few hours.

The docker-compose.yml I actually use

SQLite is fine for testing. For production, Postgres is the right call — survives container restarts cleanly, easy to back up, scales when you grow.

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

  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    container_name: n8n
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
      - N8N_HOST=${DOMAIN}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://${DOMAIN}/
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - N8N_RUNNERS_ENABLED=true
      - N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
      - GENERIC_TIMEZONE=${TZ}
      - TZ=${TZ}
      - NODE_ENV=production
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - n8n-network

volumes:
  postgres_data:
  n8n_data:

networks:
  n8n-network:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

The detail most guides skip: the 127.0.0.1:5678 binding instead of 0.0.0.0. This means n8n is reachable only via the local Nginx reverse proxy, never directly from the internet. Non-negotiable for production.

The .env file

POSTGRES_PASSWORD=$(openssl rand -base64 32)
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
DOMAIN=automation.yourdomain.com
TZ=Europe/Kyiv
Enter fullscreen mode Exit fullscreen mode

Two variables you need to internalize:

  • N8N_ENCRYPTION_KEY — encrypts every stored credential. Lose it and every saved API key, OAuth token, and password in your n8n is unrecoverable. Back it up to a password manager the moment you generate it. n8n auto-creates one if missing, but set it explicitly so you control it.
  • WEBHOOK_URL — must match your public HTTPS URL exactly. Trailing slash matters. If webhooks don't fire after setup, this is almost always why. Webhooks are core to most n8n workflows — if you're new to them, see this practical webhook primer.

Nginx + Let's Encrypt

Install Nginx and Certbot, point your A record at the VPS, then /etc/nginx/sites-available/n8n.conf:

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

server {
    listen 443 ssl http2;
    server_name automation.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/automation.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/automation.yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 50M;

    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_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;
        proxy_read_timeout 86400;
    }
}
Enter fullscreen mode Exit fullscreen mode
sudo certbot --nginx -d automation.yourdomain.com
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
Enter fullscreen mode Exit fullscreen mode

proxy_read_timeout 86400 matters — without it, long-running n8n executions get killed by Nginx. The Upgrade and Connection headers are required for the editor's WebSocket connection.

Security hardening

A self-hosted n8n with weak security is a credential theft waiting to happen. The platform stores API keys, database passwords, OAuth tokens — exactly what attackers want. Lock it down before connecting your first integration:

  • UFW firewall — allow only SSH (preferably non-default port), 80, 443. Block everything else.
  • SSH hardening — disable password auth, keys only, PermitRootLogin no. Add fail2ban.
  • Enable n8n's 2FA for the owner account immediately after first login.
  • Pin the Docker image version in production: docker.n8n.io/n8nio/n8n:1.x.x rather than :latest.
  • Restrict task runners — only set NODE_FUNCTION_ALLOW_EXTERNAL=* if absolutely necessary, never with untrusted code.
  • Auto OS updatesunattended-upgrades on Debian/Ubuntu.

Backups (the part everyone skips)

n8n's database holds workflows, credentials, and execution history. Lose it and you've lost months of work. The backup must include the PostgreSQL dump and the n8n data volume (which holds the encryption key file). Backing up only one is useless.

#!/bin/bash
# /usr/local/bin/n8n-backup.sh
BACKUP_DIR=/var/backups/n8n
DATE=$(date +%Y-%m-%d)
mkdir -p $BACKUP_DIR

# PostgreSQL dump
docker exec n8n-db pg_dump -U n8n n8n | gzip > $BACKUP_DIR/db-$DATE.sql.gz

# n8n data volume (encryption key, custom nodes)
docker run --rm -v n8n_data:/data -v $BACKUP_DIR:/backup \
    alpine tar -czf /backup/n8n-data-$DATE.tar.gz -C /data .

# Keep last 14 days locally
find $BACKUP_DIR -name "*.gz" -mtime +14 -delete

# Off-site sync
rclone copy $BACKUP_DIR remote:n8n-backups/
Enter fullscreen mode Exit fullscreen mode
# crontab -e
0 3 * * * /usr/local/bin/n8n-backup.sh >> /var/log/n8n-backup.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Test your restore process at least once. A backup you've never restored from is not a backup — it's a hope.

Update procedure

n8n ships frequently — minor versions for fixes, major versions every few months with potential breaking changes.

# Always back up first
/usr/local/bin/n8n-backup.sh

# Pull and restart
cd /opt/n8n
docker compose pull
docker compose up -d

# Verify
docker compose logs -f n8n
Enter fullscreen mode Exit fullscreen mode

Read release notes before any major upgrade. Database migrations are sometimes irreversible. Safest pattern: pin a specific version, watch the changelog, upgrade deliberately rather than auto-pulling latest.

Pitfalls that bit me (so they don't bite you)

  • Webhooks return 404 / don't fire → wrong WEBHOOK_URL. Must match the public HTTPS URL exactly, trailing slash included. Restart container after fixing.
  • OOM crashes on 1 GB VPS → add 2 GB swap. Realistically, just upgrade to 4 GB RAM.
  • "Editor unreachable" or WebSocket errors → missing Upgrade/Connection headers in Nginx. Re-check the proxy block.
  • Credential decryption failure after restore → encryption key missing from the restored data volume. Restore the full n8n_data volume, or set N8N_ENCRYPTION_KEY explicitly to the original.
  • Slow execution after a few weeks → execution history bloating the DB. Set EXECUTIONS_DATA_PRUNE=true and EXECUTIONS_DATA_MAX_AGE=336 (hours, = 14 days).
  • Backups silently failing → verify cron with grep CRON /var/log/syslog. Set up a dead-man's-switch monitor that pings you when the backup doesn't run.

Once stable, log everything that crosses webhook boundaries — debugging without it is brutal. Here's the logging pattern I use for webhooks.

When to scale (queue mode)

The single-instance setup above handles 5–8 concurrent workflows comfortably on 2 vCPU / 4 GB. Most SMBs never outgrow it — API latency, not your VPS, is the bottleneck.

If you do hit the ceiling, switch to queue mode with Redis. Add a Redis service to compose, set EXECUTIONS_MODE=queue and QUEUE_HEALTH_CHECK_ACTIVE=true, run separate worker containers. This is essentially the architecture n8n Cloud uses internally — you're just doing it on your own infra.

Verdict

Self-hosting n8n in 2026 is genuinely a great deal if you're comfortable with a Linux server. Total cost: $4–15/month including off-site backups, vs €24+/month for n8n Cloud's entry tier with execution caps. The maintenance burden after initial setup is roughly 1 hour per month — pulling an image, checking backups, occasional log review.

Switch to managed hosting only when you find yourself spending 4+ hours/month on n8n ops. At that point, $7–25/month for someone else to handle Docker pulls and backup verification is just better economics — and frees you to actually build automations.


Originally published on TrackStack — practical write-ups on automation, tracking, and infrastructure for SMBs. If your config diverges from mine, drop a comment — I read all of them and update the guide as patterns shift.

Top comments (0)