All Field Notes
N8N PLAYBOOKS · SELF-HOSTING · 10 MIN READ

Self-hosting n8n on Hetzner — the 30-minute setup we ship to clients

TL;DR · 3-LINE ANSWER

A Hetzner CX22 (€4.51/month) + Docker Compose + Caddy + Postgres 16 runs production n8n for ≈90% of SMB workloads. The setup is 30 minutes if you have an SSH key handy, and the only thing you really need to get right on day one is N8N_ENCRYPTION_KEY — change it later and every credential silently breaks. Daily encrypted Postgres backups to a separate Storage Box are non-negotiable and add €3.20/month.

This is the deployment we ship on every NexFlow client engagement that has its own VPS. We've run it on Hetzner, DigitalOcean, Linode, Scaleway, and three different bare-metal closets; the Hetzner version is the one we recommend for an SMB starting from scratch in 2026. The reasons are unglamorous: Hetzner is the cheapest credible European cloud provider, their hardware-to-price ratio is roughly 2× AWS or DigitalOcean equivalent, and their default Ubuntu image is clean enough that the rest of the setup is just Docker.

What follows is the actual docker-compose.yml, the actual Caddyfile, and the actual cron we deploy — with the four mistakes we've watched clients make when they try to self-host on their own.

Why Hetzner over DigitalOcean, AWS, or n8n Cloud

The honest answer is price-to-performance. Below are the comparable specs for "a box that runs n8n + Postgres + a small queue without thinking" in May 2026:

ProviderSpec (2 vCPU / 4 GB RAM / 40 GB)MonthlyEgress incl.
Hetzner CX22 2 vCPU AMD · 4 GB · 40 GB NVMe €4.51 20 TB
DigitalOcean Basic 2 vCPU · 4 GB · 80 GB SSD US$24 4 TB
AWS Lightsail 2 vCPU · 4 GB · 80 GB SSD US$20 4 TB
n8n Cloud Pro Managed · 20,000 executions/mo US$50

For an SMB doing 5,000–50,000 executions/month — which covers maybe 80% of NexFlow's client base — the Hetzner box is roughly 5× cheaper than the equivalent DigitalOcean droplet and clears n8n Cloud's per-execution cap at a fixed price. The trade-off is the same trade-off every self-hosted decision makes: you own the operating system. We'll come back to whether you should care.

WHEN HETZNER IS THE WRONG CHOICE

Skip Hetzner if (a) you have a regulatory requirement to keep data in North America — Hetzner's footprint is Germany, Finland, and a small Virginia presence; (b) you need 24/7 managed support with phone access — Hetzner is email-first and response times can be in hours, not minutes; (c) you're already deep in AWS for the rest of your stack. In all three cases, deploy n8n Cloud or use the same compose stack on a managed container platform like Railway or Render.

Step 1 — provision the VPS (5 minutes)

Sign in to Hetzner Cloud Console, create a new project ("nexflow-prod" works), and add your SSH public key under Security → SSH Keys before creating the server. This is the single setting that ensures password authentication is never enabled on the box.

Create a new server:

  • Location: Nuremberg (eu-central) is the canonical default. Pick Helsinki (eu-north) if your users are Scandinavian, Hillsboro (us-west) if North American.
  • Image: Ubuntu 24.04 LTS. Don't use Hetzner's "n8n app" image — we'll deploy our own compose stack so the upgrade path is in your hands.
  • Type: CX22 (shared CPU, 2 vCPU, 4 GB RAM, 40 GB NVMe, €4.51/mo). Bump to CX32 if you expect >100k executions/month.
  • Networking: IPv4 + IPv6. Public IPv4 is essential for the Let's Encrypt HTTP-01 challenge.
  • SSH key: Select the key you just added. Do not enable password authentication.
  • Backups: Skip the Hetzner-level backup checkbox (€0.45/mo for a 2-day snapshot). It is not encrypted at rest in the way you'll want for production credentials.
  • Name: n8n-prod-01. Match the hostname you'll use in DNS.

Hit Create and Hetzner provisions the box in roughly 8 seconds. Note the IPv4 address. Open a terminal:

ssh root@<your-ipv4>

You're in. Now harden it before anything else listens on a port.

Step 2 — harden the box (8 minutes)

The hardening script we run on every fresh Hetzner box is intentionally boring. It does five things and nothing else:

  • Creates a non-root user with sudo (n8nadmin), copies the SSH key, and disables root login.
  • Disables password authentication in /etc/ssh/sshd_config — keys only.
  • Installs ufw and opens 22 (SSH), 80 (HTTP for Caddy's Let's Encrypt challenge), and 443 (HTTPS). Everything else is denied.
  • Installs unattended-upgrades for security patches and reboots on the configured maintenance window. Set it to a quiet time — we use 04:15 local.
  • Installs Docker CE and Docker Compose v2 from the official Docker apt repo, not Ubuntu's older package.

The exact script is roughly 80 lines and lives in the NexFlow client handoff bundle; if you're not a client and you want a vetted version, the canonical reference is Docker's official Ubuntu install plus DigitalOcean's "initial server setup" tutorial. Both are still current for Ubuntu 24.04.

After the script: SSH back in as n8nadmin@<ip>, confirm docker --version returns 26.x or newer, and you're ready to deploy n8n.

Step 3 — deploy n8n via Docker Compose (10 minutes)

Point your domain at the Hetzner IPv4 first — an A record for n8n.yourdomain.com pointing at the server. Caddy will need this resolvable when it requests the SSL certificate.

Create the project directory: /opt/n8n. Inside it, three files: docker-compose.yml, Caddyfile, and .env.

The Compose stack

Three services in one compose file:

  • caddy — reverse proxy + automatic Let's Encrypt SSL. Image: caddy:2-alpine. Maps host ports 80 and 443 into the container. Mounts the Caddyfile read-only and a named volume for cert storage.
  • n8n — image: n8nio/n8n:1.X pinned to a specific minor version. Environment variables loaded from .env. Depends on Postgres. Listens on internal port 5678 only — never exposed to the host.
  • postgres — image: postgres:16-alpine. Stores workflow definitions, executions, and credentials. Listens on internal port 5432, also never exposed.

The .env file holds five variables that matter:

  • N8N_ENCRYPTION_KEY — 32 random characters. Generated once with openssl rand -hex 16. Never change after the first start.
  • N8N_HOSTn8n.yourdomain.com. Drives webhook URL generation.
  • WEBHOOK_URLhttps://n8n.yourdomain.com/. Trailing slash matters.
  • POSTGRES_PASSWORD — another 32-char random string.
  • N8N_BASIC_AUTH_ACTIVE=false — we'll use n8n's user-management UI instead, which is better for any team larger than one person.

The Caddyfile is a single block — Caddy handles the SSL certificate dance with Let's Encrypt automatically:

n8n.yourdomain.com {
  reverse_proxy n8n:5678
  encode gzip
  header {
    Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    X-Content-Type-Options "nosniff"
    Referrer-Policy "strict-origin-when-cross-origin"
  }
}

docker compose up -d. Caddy pulls the cert, n8n initialises the Postgres schema, and within about 60 seconds you can hit https://n8n.yourdomain.com and create your owner account. The padlock is real, the URL is yours, and you control the keys.

Step 4 — daily encrypted backups (7 minutes)

A self-hosted n8n without backups is a self-hosted ticking clock. The pattern we use on every client deployment:

  • Source: Postgres logical dump via pg_dump, run as a sidecar container on a cron schedule. Captures workflow JSON, credentials (still encrypted at rest with N8N_ENCRYPTION_KEY), and execution history.
  • Encryption: Pipe the dump through age with a public recipient key. The decryption key lives in your password manager, not on the server. If the server is fully compromised, the backups still need a second key to read.
  • Destination: A separate Hetzner Storage Box (€3.20/month for 100 GB) mounted via SFTP, or Cloudflare R2 (zero egress fees) via the S3-compatible API. Keep 30 daily and 12 monthly.
  • Verification: A separate weekly job pulls the latest backup, decrypts, and runs a sanity check — does it have the expected tables, row counts within ±20% of the source? Without this step, you discover the backup was empty on the day you needed it.

The script is 40 lines of bash. It runs in roughly 3 seconds for a database under 200 MB, and lives in /opt/n8n/scripts/backup.sh with a cron entry at 15 3 * * * (03:15 UTC nightly).

Test the restore procedure on day one, before there is anything in production worth losing. Drop a test workflow, restore the backup, verify the workflow comes back. If you can't do this in ten minutes, your backup setup isn't done.

The four mistakes we see every time

1. Rotating N8N_ENCRYPTION_KEY after the first execution

n8n encrypts every stored credential — API keys, OAuth tokens, database passwords — with this key. Change it and the stored credentials become unreadable garbage. n8n won't crash, but every workflow that needs a credential will fail with a cryptic decryption error, and the only fix is to re-enter every credential by hand. We've seen this take a small business 6 hours of work. Set the key once, back it up to your password manager, and never touch it.

2. Exposing port 5678 to the public internet

The n8n container defaults to listening on 5678. New self-hosters often bind 0.0.0.0:5678 in docker-compose.yml and hit n8n directly over HTTP. This skips SSL, skips rate limiting, and means the login page is reachable by every bot scanning the internet. Always front it with Caddy or nginx. The compose stack above does this by design — n8n's port is internal-only and the public surface is Caddy on 443.

3. Running on SQLite "for now"

n8n's default SQLite database is fine for a dev laptop. In production it deadlocks the moment two workflow executions overlap on a write, and the failure mode looks like random "database is locked" errors that bounce executions around for no obvious reason. Postgres adds 30 seconds of setup and removes an entire category of bug. Don't optimise for the day-one easy path.

4. Forgetting that webhook URLs are public

The Webhook node creates an endpoint anyone on the internet can POST to. If your workflow does anything sensitive — writes to a database, charges a card, dispatches a refund — guard it. The two simplest patterns are: include a long random token in the URL path that the workflow validates as the first node; or require a signed header (HMAC over a shared secret with the caller). n8n's "Header Auth" credential type makes the second pattern trivial. The default of "anyone with the URL can fire this" is fine for hobby projects, not production.

KEY TAKEAWAYS
  • A Hetzner CX22 (€4.51/month) + Docker Compose + Caddy + Postgres 16 runs production n8n for most SMB workloads.
  • SSH-key-only access, UFW with 22/80/443 open, and unattended-upgrades is the security baseline — non-negotiable.
  • Set N8N_ENCRYPTION_KEY once, back it up to your password manager, and never rotate it. Rotating it later breaks every credential.
  • Use Postgres from day one. SQLite deadlocks under parallel execution.
  • Daily encrypted backups to a separate object store (R2 or Hetzner Storage Box), verified weekly with a restore drill.
  • Pin n8n to a specific image tag, test minor upgrades in a staging compose stack, then promote.
  • Front n8n with Caddy or nginx — never expose port 5678 to the public internet.

Want this deployed without doing it yourself?

NexFlow ships a Spark engagement (A$2,400 one-off) that includes the Hetzner deployment, your first production workflow, and a 14-day shadow run. You own the box, the code, and the encryption key from day one.

Sources & method

  1. Hetzner pricing from hetzner.com/cloud as of May 2026.
  2. Docker install instructions: docs.docker.com/engine/install/ubuntu.
  3. n8n environment variables reference: docs.n8n.io/hosting.
  4. Caddy automatic HTTPS documentation: caddyserver.com/docs/automatic-https.
  5. Deployment patterns drawn from 47 NexFlow client engagements completed between Jan 2024 and Apr 2026. Setup time of "≈30 minutes" is a median; first-time self-hosters typically take 60–90 minutes.