Skip to main content
Syed Noor

How to Self-Host n8n on Hetzner for Under $20/Month

Step-by-step guide to self-hosting n8n on Hetzner with Docker Compose, PostgreSQL, Caddy SSL, automated backups, and monitoring — all for under $20/month.

Monthly cost comparison: self-hosted n8n vs cloud platforms Horizontal bar chart comparing monthly automation costs. Zapier: $400-700 per month. Make: $200-300 per month. n8n Cloud: $50-100 per month. n8n Self-Hosted on Hetzner: $18 per month. Annual savings of $4,764 to $8,184 compared to Zapier. Monthly Automation Cost MID-VOLUME E-COMMERCE WORKLOAD ZAPIER $400–700/mo MAKE $200–300/mo N8N CLOUD $50–100/mo HETZNER $18/mo Annual savings vs Zapier: $4,584 – $8,184 / year FIG · MONTHLY PLATFORM COST COMPARISON

The most common objection I hear from teams evaluating n8n self-hosted is: “We do not have the DevOps capacity to run our own infrastructure.” This guide shows you the technical steps involved — so you can make an informed decision about whether to DIY or hand it off.

Prefer managed hosting? If you don’t want to manage your own infrastructure, n8n Cloud handles everything for you — no Docker, no server maintenance. Start with their free trial.

Fair warning: getting the basic containers running is the easy part. The hard part — and the part most guides skip — is everything after: production-grade error handling, security hardening, backup verification, monitoring that actually alerts you, and the workflows themselves built with idempotency and retry logic. That is the difference between “it runs” and “it runs in production.”

This guide covers the infrastructure layer. By the end, you will have n8n running on Hetzner with PostgreSQL, automatic SSL via Caddy, encrypted offsite backups, and basic monitoring — for $18/month all-in. What it does NOT cover is the workflow-level production discipline that takes most teams weeks to get right.

import BlogVizHetznerCost from ’../../components/blog/BlogVizHetznerCost.astro’;


Why Hetzner

Hetzner is a German hosting provider with data centers in Falkenstein, Nuremberg, Helsinki, and Ashburn (US). Their pricing is roughly 60% cheaper than equivalent AWS or DigitalOcean instances, and their EU data centers make GDPR data residency straightforward.

For n8n, the CX22 shared vCPU instance is the sweet spot:

ResourceCX22 Spec
vCPU2 cores
RAM4 GB
Storage40 GB NVMe
Traffic20 TB/month
Price$4.50/month

This handles most n8n workloads comfortably — up to several hundred workflow executions per day with PostgreSQL running on the same instance. When you outgrow it, Hetzner’s vertical scaling lets you bump to CX32 (8 GB RAM, $7.50/month) without migration.

Total monthly cost breakdown:

ItemCost
Hetzner CX22$4.50
Hetzner 40 GB backup space$2.40
Domain (amortized)~$1.00
Monitoring (UptimeRobot free tier)$0.00
Total~$8-10/month

Even with a larger CX32 instance and paid monitoring, you stay well under $20/month. Compare that to Zapier at $400-700/month for a mid-volume e-commerce operation, or n8n Cloud at $50-100/month.


Prerequisites

Before starting:

  1. A Hetzner account. Sign up at hetzner.com. You need a payment method on file.
  2. A domain name. Point an A record (e.g., n8n.yourdomain.com) to your server IP after provisioning.
  3. An SSH key pair. If you do not have one: ssh-keygen -t ed25519 -C "n8n-server".
  4. Basic terminal familiarity. You should be comfortable running commands over SSH.

Step 1: Provision the Server

In Hetzner Cloud Console:

  1. Create a new project (e.g., “n8n-production”)
  2. Add your SSH public key under Security > SSH Keys
  3. Create a server:
    • Location: Falkenstein (cheapest) or Helsinki (if you need Nordic data residency)
    • Image: Ubuntu 24.04
    • Type: CX22 (Shared vCPU, 2 cores, 4 GB RAM)
    • SSH Key: Select the key you added
    • Name: n8n-prod

The server provisions in about 30 seconds. Note the IP address.

Point your domain:

Add an A record in your DNS provider:

n8n.yourdomain.com  →  YOUR_SERVER_IP

DNS propagation takes 5-30 minutes. Proceed with server setup while it propagates.


Step 2: Initial Server Hardening

SSH into your server and run the baseline hardening:

ssh root@YOUR_SERVER_IP

# Update packages
apt update && apt upgrade -y

# Create a non-root user
adduser n8n --disabled-password --gecos ""
usermod -aG sudo docker n8n

# Install Docker
curl -fsSL https://get.docker.com | sh

# Add user to docker group
usermod -aG docker n8n

# Install Docker Compose plugin
apt install docker-compose-plugin -y

# Configure firewall
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

# Disable password authentication (SSH key only)
sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart sshd

Enable automatic security updates:

apt install unattended-upgrades -y
dpkg-reconfigure -plow unattended-upgrades

Step 3: Docker Compose Setup

Create the project directory and the compose file:

su - n8n
mkdir -p ~/n8n-stack && cd ~/n8n-stack

Create the docker-compose.yml:

version: "3.8"

services:
  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=${N8N_DOMAIN}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://${N8N_DOMAIN}/
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - N8N_BASIC_AUTH_ACTIVE=false
      - N8N_DIAGNOSTICS_ENABLED=false
      - GENERIC_TIMEZONE=UTC
      - TZ=UTC
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - n8n-net

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

  caddy:
    image: caddy:2-alpine
    container_name: n8n-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - n8n
    networks:
      - n8n-net

volumes:
  n8n_data:
  postgres_data:
  caddy_data:
  caddy_config:

networks:
  n8n-net:
    driver: bridge

Key decisions in this config:

  • n8n binds to 127.0.0.1:5678 — not publicly accessible. Caddy handles external traffic and SSL termination.
  • PostgreSQL 16 instead of SQLite — required for production. SQLite locks under concurrent writes and does not support n8n’s queue mode.
  • Health check on Postgres — n8n waits until the database is actually ready, not just until the container starts.
  • N8N_DIAGNOSTICS_ENABLED=false — no telemetry sent to n8n GmbH. Your data stays on your server.

Step 4: Environment Variables

Create the .env file:

# Generate secure passwords
POSTGRES_PASSWORD=$(openssl rand -hex 24)
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)

cat > .env << EOF
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
N8N_DOMAIN=n8n.yourdomain.com
EOF

# Lock down permissions
chmod 600 .env

Critical: The N8N_ENCRYPTION_KEY encrypts all stored credentials in n8n. If you lose this key, you lose access to every credential stored in your instance. Back it up separately — I recommend a password manager entry.


Step 5: Caddy Reverse Proxy with Automatic SSL

Create the Caddyfile:

n8n.yourdomain.com {
    reverse_proxy n8n:5678 {
        flush_interval -1
    }

    header {
        # Security headers
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"

        # Remove server identification
        -Server
    }

    log {
        output file /data/access.log {
            roll_size 10mb
            roll_keep 5
        }
    }
}

Caddy automatically provisions and renews Let’s Encrypt certificates. No certbot, no cron jobs, no renewal failures at 3 AM. The flush_interval -1 setting is required for n8n’s server-sent events (SSE) used by the editor’s real-time updates.


Step 6: Launch

cd ~/n8n-stack
docker compose up -d

Watch the logs to confirm everything starts cleanly:

docker compose logs -f

You should see:

  1. PostgreSQL starting and passing health checks
  2. n8n connecting to PostgreSQL and running migrations
  3. Caddy provisioning the SSL certificate
  4. n8n reporting “Editor is now accessible via: https://n8n.yourdomain.com/

Open https://n8n.yourdomain.com in your browser. You will be prompted to create your owner account — this is your admin user. Use a strong password and save it in your password manager.


Step 7: Automated Backups with Restic

A database without backups is a liability. Restic provides encrypted, deduplicated backups to any S3-compatible storage. Hetzner’s Storage Box or Backblaze B2 both work well.

Install restic:

sudo apt install restic -y

Initialize the backup repository (using Hetzner Storage Box as example):

export RESTIC_REPOSITORY="sftp:uXXXXXX@uXXXXXX.your-storagebox.de:/n8n-backups"
export RESTIC_PASSWORD="your-restic-encryption-password"

restic init

Create the backup script at ~/n8n-stack/backup.sh:

#!/bin/bash
set -euo pipefail

BACKUP_DIR="/tmp/n8n-backup-$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

# Dump PostgreSQL
docker exec n8n-postgres pg_dump -U n8n -d n8n -F custom \
  -f /tmp/n8n-db.dump
docker cp n8n-postgres:/tmp/n8n-db.dump "$BACKUP_DIR/n8n-db.dump"
docker exec n8n-postgres rm /tmp/n8n-db.dump

# Copy n8n data volume
docker cp n8n:/home/node/.n8n "$BACKUP_DIR/n8n-data"

# Copy .env and compose file (for disaster recovery)
cp ~/n8n-stack/.env "$BACKUP_DIR/"
cp ~/n8n-stack/docker-compose.yml "$BACKUP_DIR/"
cp ~/n8n-stack/Caddyfile "$BACKUP_DIR/"

# Send to restic
export RESTIC_REPOSITORY="sftp:uXXXXXX@uXXXXXX.your-storagebox.de:/n8n-backups"
export RESTIC_PASSWORD="your-restic-encryption-password"

restic backup "$BACKUP_DIR" --tag n8n-daily

# Prune old backups: keep 7 daily, 4 weekly, 3 monthly
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 3 --prune

# Cleanup
rm -rf "$BACKUP_DIR"

echo "[$(date)] Backup completed successfully"
chmod +x ~/n8n-stack/backup.sh

Schedule it with cron (daily at 3 AM UTC):

crontab -e
# Add:
0 3 * * * /home/n8n/n8n-stack/backup.sh >> /home/n8n/n8n-stack/backup.log 2>&1

Test the backup immediately:

~/n8n-stack/backup.sh
restic snapshots

If the snapshot appears with the correct size, your backup pipeline works. Test a restore on a staging instance at least once — a backup you have never restored from is a backup you cannot trust.


Step 8: Basic Monitoring

Set up three monitoring layers:

Layer 1 — UptimeRobot (free tier):

Create an account at uptimerobot.com. Add an HTTP(s) monitor for https://n8n.yourdomain.com/healthz. Set the check interval to 5 minutes. Configure alerts to email or Slack. This catches server crashes, Docker failures, and SSL expiration.

Layer 2 — Docker health monitoring:

Add a simple health check script at ~/n8n-stack/health-check.sh:

#!/bin/bash
CONTAINERS=("n8n" "n8n-postgres" "n8n-caddy")

for container in "${CONTAINERS[@]}"; do
  status=$(docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null)
  if [ "$status" != "running" ]; then
    echo "ALERT: Container $container is $status" | \
      mail -s "n8n Container Alert" your-email@example.com
  fi
done

Schedule it every 10 minutes via cron.

Layer 3 — Disk and memory alerts:

# Add to crontab — alert if disk > 85% or memory > 90%
*/30 * * * * [ $(df / --output=pcent | tail -1 | tr -d ' %') -gt 85 ] && echo "Disk alert" | mail -s "n8n Disk Alert" your-email@example.com

Step 9: Updates

n8n releases frequently. To update:

cd ~/n8n-stack

# Pull latest images
docker compose pull

# Restart with new images
docker compose up -d

# Verify
docker compose logs -f n8n

Always back up before updating. n8n database migrations are forward-only — if a new version introduces a breaking change, you need the backup to roll back.

Pin to a specific major version if stability matters more than features:

# In docker-compose.yml, change:
image: docker.n8n.io/n8nio/n8n:latest
# To:
image: docker.n8n.io/n8nio/n8n:1.93

What You Get for $18/Month

FeatureIncluded
n8n with PostgreSQLYes
Automatic SSL (Let’s Encrypt)Yes
Daily encrypted backupsYes
Uptime monitoringYes
2 vCPU, 4 GB RAMYes
20 TB bandwidthYes
EU data residencyYes (Falkenstein/Helsinki)
Per-execution pricingNo — unlimited

Compare to Zapier at $400-700/month for equivalent workflow volume. Compare to n8n Cloud at $50-100/month. The self-hosted route is 95% cheaper, gives you full data sovereignty, and — once set up — requires about 30 minutes of maintenance per month for updates and backup verification.


Common Issues and Fixes

n8n cannot connect to PostgreSQL: Check that the PostgreSQL container is healthy: docker compose ps. If it shows “starting” or “unhealthy,” check logs: docker compose logs postgres. Most common cause: the .env file has the wrong password or is not readable.

SSL certificate fails to provision: Caddy needs ports 80 and 443 open. Verify: ufw status. Also verify your DNS A record has propagated: dig n8n.yourdomain.com. If you just created the record, wait 10-30 minutes.

n8n editor loads but webhooks do not fire: Check that WEBHOOK_URL in your .env matches your actual domain with https://. Caddy’s flush_interval -1 must be set for SSE to work.

Out of disk space: Docker images and execution logs accumulate. Prune unused images: docker system prune -f. If execution logs are the issue, configure EXECUTIONS_DATA_MAX_AGE in your n8n environment variables (e.g., 168 for 7 days).


Why Most Teams Hire This Out

If you followed this guide top to bottom and everything worked — congratulations, you are in the minority. In practice, most teams hit 2-3 of these roadblocks:

  • DNS propagation delays that block SSL for hours while the business waits.
  • Docker networking issues specific to their VPS provider or firewall setup.
  • PostgreSQL tuning that the defaults get wrong for n8n’s write-heavy workload.
  • Backup scripts that silently fail because of permissions, disk space, or credential expiry — discovered only when you actually need the backup.
  • Security gaps this guide does not cover: fail2ban, unattended-upgrades, credential encryption at rest, webhook HMAC validation, rate limiting.
  • The workflow layer — idempotency, dead-letter queues, structured audit trails, environment-based credential management — that takes more engineering time than the infrastructure itself.

The infrastructure in this guide takes an experienced DevOps engineer 45-60 minutes. Getting it production-grade — with all the security, monitoring, and workflow discipline layered on — takes 2-3 days. That is the gap the noorflows Self-Hosted Setup ($997) fills.

What You Get vs What This Guide Covers

This GuideSelf-Hosted Setup ($997)
Docker + PostgreSQL + SSLYesYes
BackupsBasic scriptVerified + monitored + tested restore
Security hardeningMinimal (UFW + SSH keys)Full: fail2ban, TLS 1.3, HMAC webhooks, rate limiting, CSP headers
MonitoringUptimeRobot pingExecution dashboards, failure alerting, anomaly detection
Workflow migrationNoYes — rebuilt with production patterns
DocumentationThis blog postCustom runbook for your team
Support windowCommunity forum30-day direct support
Time to production2-3 days (if no issues)5 business days, guaranteed

What to Do Next

If you are comfortable managing your own Docker infrastructure and just needed the config — you now have it. Read the 6-Dimension Production-Readiness Checklist to make sure the workflows running on this infrastructure are built to last.

If you want the complete package — infrastructure provisioned, hardened, monitored, documented, and workflows migrated with production discipline — the noorflows Self-Hosted Setup ($997) delivers all of it in 5 business days. No DevOps capacity required on your side.

If you are not sure which route fits, email me. I will tell you honestly whether DIY makes sense for your team — and if it does, this guide is my gift. No hard sell.

Get in touch