DEV Community

Nevik Schmidt
Nevik Schmidt

Posted on

n8n Self-Hosting on Hetzner: Complete Docker Setup Guide (2026)

I've been self-hosting n8n for 18 months on a Hetzner VPS. Here's my complete production setup—from zero to running workflows in 30 minutes.

Why Hetzner + n8n?

Hetzner CX22 (€5/month):

  • 2 vCPU
  • 4 GB RAM
  • 40 GB disk
  • Location: Falkenstein (EU, GDPR-friendly)

n8n self-hosted:

  • Unlimited workflows
  • No execution limits
  • Full data control
  • All integrations included

Compare to n8n.cloud:

  • Starter: €20/month (5,000 executions)
  • Pro: €50/month (100,000 executions)

My setup runs 150,000+ executions/month on a €5 VPS. That's €45/month saved.

Prerequisites

  • Hetzner account (signup takes 5 minutes)
  • Domain (optional but recommended)
  • SSH key for server access
  • 30 minutes time

Step 1: Create the Server

  1. Go to Hetzner Cloud Console
  2. Create new project "n8n-production"
  3. Add SSH key (Settings → Security → SSH Keys)
  4. Create server:
    • Location: Falkenstein or Nuremberg
    • Image: Ubuntu 24.04
    • Type: CX22 (€5/month)
    • Networking: IPv4 + IPv6
    • SSH Key: Select your key

Note the IP address. We'll need it.

Step 2: Initial Server Setup

SSH into your server:

ssh root@YOUR_SERVER_IP
Enter fullscreen mode Exit fullscreen mode

Update and install Docker:

# Update system
apt update && apt upgrade -y

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

# Add user for n8n (security best practice)
useradd -m -s /bin/bash n8n
usermod -aG docker n8n

# Create directories
mkdir -p /opt/n8n/data /opt/n8n/postgres /opt/traefik
chown -R n8n:n8n /opt/n8n
Enter fullscreen mode Exit fullscreen mode

Step 3: Docker Compose Configuration

Create /opt/n8n/docker-compose.yml:

version: '3.8'

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

  n8n:
    image: n8nio/n8n:latest
    container_name: n8n-app
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    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_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=${N8N_USER}
      - N8N_BASIC_AUTH_PASSWORD=${N8N_PASSWORD}
      - N8N_HOST=${N8N_DOMAIN}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://${N8N_DOMAIN}/
      - GENERIC_TIMEZONE=Europe/Berlin
      - TZ=Europe/Berlin
    volumes:
      - ./data:/home/node/.n8n
    ports:
      - "127.0.0.1:5678:5678"
    networks:
      - n8n-network

  traefik:
    image: traefik:v3.0
    container_name: traefik
    restart: unless-stopped
    command:
      - "--api.dashboard=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - n8n-network
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik.rule=Host(`traefik.${N8N_DOMAIN}`)"
      - "traefik.http.routers.traefik.entrypoints=websecure"
      - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_AUTH}"

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

Step 4: Environment Variables

Create /opt/n8n/.env:

# PostgreSQL
POSTGRES_PASSWORD=$(openssl rand -hex 32)

# n8n Auth
N8N_USER=admin
N8N_PASSWORD=$(openssl rand -hex 16)

# Domain (CHANGE THIS!)
N8N_DOMAIN=n8n.yourdomain.com
ACME_EMAIL=your@email.com

# Traefik Dashboard Auth (generate with htpasswd)
TRAEFIK_AUTH=admin:$$apr1$$xyz...
Enter fullscreen mode Exit fullscreen mode

Generate passwords:

cd /opt/n8n

# Generate random passwords
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" > .env
echo "N8N_USER=admin" >> .env
echo "N8N_PASSWORD=$(openssl rand -hex 16)" >> .env

# Add your domain
read -p "Enter your domain (e.g., n8n.example.com): " DOMAIN
echo "N8N_DOMAIN=$DOMAIN" >> .env
echo "ACME_EMAIL=admin@$DOMAIN" >> .env

# Generate Traefik auth
TRAEFIK_PASS=$(openssl rand -hex 8)
TRAEFIK_HASH=$(htpasswd -nb admin $TRAEFIK_PASS | sed 's/\$/\$\$/g')
echo "TRAEFIK_AUTH=$TRAEFIK_HASH" >> .env

echo "Save these credentials!"
cat .env
Enter fullscreen mode Exit fullscreen mode

Step 5: DNS Configuration

Go to your domain registrar and add:

A    n8n.yourdomain.comYOUR_SERVER_IP
A    traefik.n8n.yourdomain.comYOUR_SERVER_IP
Enter fullscreen mode Exit fullscreen mode

Wait 5-10 minutes for DNS propagation.

Step 6: Start Everything

cd /opt/n8n

# Start services
docker compose up -d

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

First start takes 2-3 minutes (PostgreSQL initialization).

Step 7: Access n8n

Open your browser: https://n8n.yourdomain.com

Login with:

  • User: admin
  • Password: (from your .env file)

Step 8: Configure Backups

Don't skip this. Here's my backup script:

#!/bin/bash
# /opt/n8n/backup.sh

BACKUP_DIR="/backup/n8n"
DATE=$(date +%Y%m%d_%H%M%S)

# Create backup directory
mkdir -p $BACKUP_DIR

# Backup PostgreSQL
docker exec n8n-postgres pg_dump -U n8n n8n > $BACKUP_DIR/db_$DATE.sql

# Backup n8n data
tar -czf $BACKUP_DIR/data_$DATE.tar.gz /opt/n8n/data

# Upload to Backblaze B2 (€0.005/GB)
rclone copy $BACKUP_DIR b2:n8n-backups/

# Keep only last 30 days locally
find $BACKUP_DIR -type f -mtime +30 -delete

echo "Backup completed: $DATE"
Enter fullscreen mode Exit fullscreen mode

Add to crontab:

crontab -e
0 2 * * * /opt/n8n/backup.sh >> /var/log/n8n-backup.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Step 9: Security Hardening

Firewall Setup

# Install UFW
apt install ufw -y

# Allow only necessary ports
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow http
ufw allow https
ufw enable
Enter fullscreen mode Exit fullscreen mode

Fail2Ban

apt install fail2ban -y
systemctl enable fail2ban
systemctl start fail2ban
Enter fullscreen mode Exit fullscreen mode

Automatic Updates

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

Step 10: Monitoring

I use this simple health check workflow in n8n itself:

Trigger: Every 5 minutes
  ↓
HTTP Request: GET https://n8n.yourdomain.com/healthz
  ↓
Switch:
  - Status 200 → Do nothing
  - Status != 200 → Telegram alert
Enter fullscreen mode Exit fullscreen mode

Self-healing with watchdog:

# /opt/n8n/watchdog.sh
#!/bin/bash
if ! docker ps | grep -q n8n-app; then
  cd /opt/n8n
  docker compose up -d
  echo "n8n restarted at $(date)" >> /var/log/n8n-watchdog.log
fi
Enter fullscreen mode Exit fullscreen mode
crontab -e
*/5 * * * * /opt/n8n/watchdog.sh
Enter fullscreen mode Exit fullscreen mode

Maintenance Commands

# View logs
docker compose logs -f n8n

# Restart n8n
docker compose restart n8n

# Update n8n
docker compose pull n8n
docker compose up -d n8n

# Full backup
docker compose exec postgres pg_dump -U n8n n8n > backup.sql

# Restore from backup
cat backup.sql | docker compose exec -T postgres psql -U n8n n8n
Enter fullscreen mode Exit fullscreen mode

Performance Tuning

For heavy workloads (1000+ executions/hour), add to docker-compose.yml:

n8n:
  deploy:
    resources:
      limits:
        memory: 2G
      reservations:
        memory: 1G
Enter fullscreen mode Exit fullscreen mode

Cost Summary

Item Monthly Cost
Hetzner CX22 €5
Domain €1
Backblaze B2 (10GB) €0.05
Total €6.05

Compare to n8n.cloud Pro (€50/month). Savings: €43.95/month = €527/year.

Troubleshooting

n8n won't start

docker compose logs n8n | tail -50
# Usually: database connection issue or port conflict
Enter fullscreen mode Exit fullscreen mode

SSL certificate issues

docker compose logs traefik | grep acme
# Check: DNS propagated? Port 80 accessible?
Enter fullscreen mode Exit fullscreen mode

Slow workflows

  • Check PostgreSQL disk usage: docker compose exec postgres df -h
  • Increase memory limits
  • Enable workflow execution history cleanup

Get Help

If you want this setup but don't have time:

👉 Done-for-you n8n Setup: https://nevki.de

I'll set up:

  • Complete Docker deployment
  • SSL + domain configuration
  • Backup automation
  • Security hardening
  • 30-minute training call

Price: €299 one-time (or €6.05/month self-managed)

Free Workflow Templates

👉 Production-ready n8n workflows: https://nevikschmidt.gumroad.com/l/uhrqpe

Includes:

  • WordPress automation pack
  • Invoice processing
  • Social media scheduling
  • Email marketing automation

Running in production since October 2024. 99.9% uptime. Zero data loss.

Top comments (0)