DEV Community

Syed Noor
Syed Noor

Posted on • Originally published at noorflows.com

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

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:

Resource CX22 Spec
vCPU 2 cores
RAM 4 GB
Storage 40 GB NVMe
Traffic 20 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:

Item Cost
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Enable automatic security updates:

apt install unattended-upgrades -y
dpkg-reconfigure -plow unattended-upgrades
Enter fullscreen mode Exit fullscreen mode

Step 3: Docker Compose Setup

Create the project directory and the compose file:

su - n8n
mkdir -p ~/n8n-stack && cd ~/n8n-stack
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Watch the logs to confirm everything starts cleanly:

docker compose logs -f
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
chmod +x ~/n8n-stack/backup.sh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Test the backup immediately:

~/n8n-stack/backup.sh
restic snapshots
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What You Get for $18/Month

Feature Included
n8n with PostgreSQL Yes
Automatic SSL (Let’s Encrypt) Yes
Daily encrypted backups Yes
Uptime monitoring Yes
2 vCPU, 4 GB RAM Yes
20 TB bandwidth Yes
EU data residency Yes (Falkenstein/Helsinki)
Per-execution pricing No — 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 Guide Self-Hosted Setup ($997)
Docker + PostgreSQL + SSL Yes Yes
Backups Basic script Verified + monitored + tested restore
Security hardening Minimal (UFW + SSH keys) Full: fail2ban, TLS 1.3, HMAC webhooks, rate limiting, CSP headers
Monitoring UptimeRobot ping Execution dashboards, failure alerting, anomaly detection
Workflow migration No Yes — rebuilt with production patterns
Documentation This blog post Custom runbook for your team
Support window Community forum 30-day direct support
Time to production 2-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.

Top comments (0)