To install n8n on Hetzner, rent a CX23 (2 vCPU, 4 GB RAM), lock down SSH, install Docker, and start n8n plus Postgres with one compose file behind Caddy for HTTPS. On our production server (CX23, Helsinki, Ubuntu 26.04.1) Docker installed in 22 s and n8n 2.46.6 went from image pull to healthy in 105 s. Idle, the whole server used 1,036 MiB of RAM.

This matters now because n8n 2.x renamed and deprecated several settings that older Hetzner tutorials still use, and Ubuntu 24.04 and later changed how SSH restarts. Below is every step in the order we ran it on 2026-11-05, including what broke. It belongs to our n8n hosting guide.

Tested in the Lab Hetzner CX23 (production, Ubuntu 26.04) · Nov 2026

What you end up with

  • n8n 2.46.6 and PostgreSQL 16 in Docker, image tag pinned, data in named volumes.
  • n8n listening on 127.0.0.1:5678 only, so the internet never reaches it directly.
  • Caddy in front with an automatic Let's Encrypt certificate for your subdomain.
  • Key-only SSH, a firewall that allows 22, 80 and 443, fail2ban and automatic security updates.

Our measured install

These numbers come from the install itself, measured with docker stats --no-stream and zero workflows. RAM is in MiB (1 MiB = 1,048,576 bytes, about 5% more than a "MB").

n8n 2.46.6 on Hetzner CX23Measured
Docker install22 s
Image pull + first start to healthy105 s
n8n RAM right after start785.3 MiB
n8n RAM idle (after ~60 s)379.7 MiB
PostgreSQL 16 RAM idle69.6 MiB
Whole server RAM used, idle1036 MiB of 3,814
n8n image size1.94GB
Disk used after install4.7G of 38G

Measured by AI Hosting Lab on 2026-11-05 · docker compose: n8n + postgres:16, docker.io from Ubuntu repo, task runners on · method

Two readings matter for sizing. n8n peaks at 785.3 MiB right after start, then settles to 379.7 MiB after about a minute. So a 2 GB server would boot n8n, but the 4 GB CX23 leaves room for workflows, Postgres and the OS. If you want to size for your own workload, try the VPS RAM calculator.

Step 1: Create the server

In the Hetzner Cloud console (Hetzner Cloud) pick location Helsinki (or the one nearest your users), image Ubuntu 26.04, type CX23. Add your SSH public key in the same dialog so the server never needs a root password. If you do not have a key yet, create one on your own computer:

ssh-keygen -t ed25519 -C "n8n-server"
cat ~/.ssh/id_ed25519.pub

Paste the .pub line into Hetzner. Then log in once to confirm the key works:

ssh root@YOUR_SERVER_IP

Step 2: Update and harden before anything else

We ran our hardening.sh as root while the first SSH session stayed open. It upgrades packages, installs unattended-upgrades, ufw and fail2ban, writes a key-only SSH config, opens ports 22, 80 and 443, and enables the firewall. The full script with a line-by-line explanation is in how to secure an Ubuntu VPS. The SSH part is short:

PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password

What broke: systemctl reload ssh failed with ssh.service is not active. That is expected on Ubuntu 24.04 and later: sshd is started per connection through ssh.socket, so there is no running service to reload. New connections already read the new file. We checked it from a second machine; a password-only login now ends with Permission denied (publickey).

The upgrade pulled a new kernel and libraries, so we rebooted:

[ -f /var/run/reboot-required ] && systemctl reboot

Step 3: Install Docker (22 s)

We used Ubuntu's own packages because they install in one command and get security updates through the same unattended-upgrades as the rest of the system:

apt-get -y install docker.io docker-compose-v2
systemctl enable --now docker

On our CX23 that took 22 s. Be aware of the trade-off: Docker's own documentation calls docker.io and docker-compose-v2 unofficial packages and offers its own docker-ce repository with newer releases. For a single n8n stack, the Ubuntu packages did everything we needed.

Step 4: Write the .env file

mkdir -p /opt/n8n && cd /opt/n8n
cat > .env <<'EOF'
POSTGRES_USER=n8n
POSTGRES_PASSWORD=change-me-openssl-rand-hex-24
POSTGRES_DB=n8n
N8N_ENCRYPTION_KEY=change-me-openssl-rand-hex-32
EOF
chmod 600 .env
openssl rand -hex 24   # paste as POSTGRES_PASSWORD
openssl rand -hex 32   # paste as N8N_ENCRYPTION_KEY

Replace both placeholders. Store a copy of N8N_ENCRYPTION_KEY in your password manager: n8n encrypts saved credentials with it, and a database backup without the key cannot decrypt them.

Step 5: The compose file we run

Save this as /opt/n8n/compose.yml. Change the domain to yours and the timezone to your own.

services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    env_file: .env
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
      interval: 10s
      timeout: 5s
      retries: 10
  n8n:
    image: docker.n8n.io/n8nio/n8n:2.46.6
    restart: unless-stopped
    env_file: .env
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      N8N_HOST: n8n.aihostinglab.com
      N8N_PROTOCOL: https
      WEBHOOK_URL: https://n8n.aihostinglab.com/
      N8N_EDITOR_BASE_URL: https://n8n.aihostinglab.com/
      N8N_PROXY_HOPS: "1"
      GENERIC_TIMEZONE: Europe/Prague
      TZ: Europe/Prague
      N8N_RUNNERS_ENABLED: "true"
      N8N_DIAGNOSTICS_ENABLED: "false"
      N8N_PERSONALIZATION_ENABLED: "false"
      EXECUTIONS_DATA_PRUNE: "true"
      EXECUTIONS_DATA_MAX_AGE: "336"
    ports:
      - "127.0.0.1:5678:5678"
    volumes:
      - n8ndata:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy
volumes:
  pgdata:
  n8ndata:

Three choices in this file do most of the work:

  • Pinned tag 2.46.6, not latest. A restart never upgrades n8n behind your back.
  • 127.0.0.1:5678:5678. Docker's published ports skip ufw rules (Docker documents this), so a plain 5678:5678 would put n8n on the internet even with the firewall on. From outside, port 5678 on our server does not answer.
  • depends_on with service_healthy. n8n waits until Postgres answers pg_isready, so the first start does not fail on a cold database.

We checked our own file against the current n8n docs, and you should know what we found. WEBHOOK_URL is deprecated from n8n 2.35.0 in favour of N8N_WEBHOOK_URL; it still works but logs a warning at start. N8N_RUNNERS_ENABLED is deprecated from n8n 2.0. The two EXECUTIONS_DATA_* lines repeat the defaults (prune on, 336 hours = 14 days). And N8N_DIAGNOSTICS_ENABLED=false also disables Ask AI in the Code node. We publish the file exactly as it runs; on a new install, use N8N_WEBHOOK_URL.

Step 6: Start it (105 s to healthy)

cd /opt/n8n
docker compose up -d
docker compose ps
curl -s http://127.0.0.1:5678/healthz

The first start pulls about 1.94 GB for n8n and 642 MB for Postgres. On our CX23, pull plus first start until /healthz answered {"status":"ok"} took 105 s. After the install, the disk showed 4.7G of 38G used.

Step 7: DNS and HTTPS with Caddy

Create an A record for your subdomain pointing at the server IP. If the domain is on Cloudflare, set that record to DNS only (proxy off), or the certificate challenge goes to Cloudflare instead of your server. Then install Caddy from the Ubuntu repository and point it at n8n:

apt-get -y install caddy
nano /etc/caddy/Caddyfile
systemctl reload caddy

Our full Caddyfile, the proxy variables and the checks we ran are on the next page of this guide; this page stays with the install itself.

Step 8: Create the owner account immediately

As soon as the domain works, n8n shows an owner-setup page. Until someone fills it in, the first visitor can claim the instance. Open your URL and create the owner account right away, with a long password from your password manager.

Updating later

cd /opt/n8n
docker compose exec postgres pg_dump -U n8n n8n > n8n-$(date +%F).sql
# edit the image tag in compose.yml, e.g. 2.46.6 -> the new version
docker compose pull
docker compose up -d

Read the n8n release notes before a jump across minor versions, and keep the dump plus your .env off the server.

What broke, what we avoided, and the fix

SymptomCauseFix
systemctl reload ssh: "ssh.service is not active"Ubuntu 24.04+ starts sshd through ssh.socketNothing to reload; test a new login. Run sshd -t first to catch typos.
Risk we avoided: certificate challenge landing at CloudflareA proxied record answers DNS with Cloudflare IPs, so the ACME check reaches Cloudflare firstWe set the record to DNS only before starting Caddy
Owner-setup page open to anyoneFresh instance without an accountCreate the owner right after the domain goes live

Skip this guide if

  • You do not want to apply OS updates and n8n upgrades yourself. Managed n8n Cloud removes that work for a monthly fee.
  • You need queue mode with workers. This is a single-instance setup.

Next step: put this instance on the internet safely with our Caddy and HTTPS setup for n8n.

FAQ

Which Hetzner server do I need for n8n?

We run n8n 2.46.6 with Postgres 16 on a CX23 (2 vCPU, 4 GB RAM). Idle, the whole server used 1,036 MiB of RAM, so there is room for normal workflows. Heavy AI or many parallel executions need more; we have not load-tested the CX23 yet.

Should I use SQLite or Postgres for self-hosted n8n?

SQLite works for testing. We use Postgres 16 because it runs as its own service with a health check and is easy to back up with pg_dump. Postgres used 69.6 MiB of RAM idle on our CX23.

Can I use the Hetzner Docker CE app image instead?

Yes. The official n8n Hetzner guide uses it and adds the compose plugin. We started from plain Ubuntu 26.04 and installed Docker from Ubuntu's own packages, which took 22 seconds.

How do I update n8n after installing it this way?

Back up Postgres, change the pinned image tag in the compose file, then run docker compose pull and docker compose up -d. Pinning the tag means an update only happens when you decide.

Why does n8n show a setup page after install?

A fresh n8n instance shows an owner-setup page until the first account exists. Anyone who reaches the URL first can claim it, so create the owner account right after you expose the instance.

Sources

  1. n8n Docs, Deploy to Hetzner (2026)
  2. n8n Docs, Deployment environment variables (2026)
  3. n8n Docs, Endpoints environment variables (2026)
  4. n8n Docs, Task runner environment variables (2026)
  5. n8n Docs, Executions environment variables (2026)
  6. Docker Docs, Install Docker Engine on Ubuntu (2026)
  7. Docker Docs, Packet filtering and firewalls (2026)
  8. Ubuntu Discourse, SSHd now uses socket-based activation (2022)