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
- Go to Hetzner Cloud Console
- Create new project "n8n-production"
- Add SSH key (Settings → Security → SSH Keys)
- 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
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
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
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...
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
Step 5: DNS Configuration
Go to your domain registrar and add:
A n8n.yourdomain.com → YOUR_SERVER_IP
A traefik.n8n.yourdomain.com → YOUR_SERVER_IP
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
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"
Add to crontab:
crontab -e
0 2 * * * /opt/n8n/backup.sh >> /var/log/n8n-backup.log 2>&1
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
Fail2Ban
apt install fail2ban -y
systemctl enable fail2ban
systemctl start fail2ban
Automatic Updates
apt install unattended-upgrades -y
dpkg-reconfigure -plow unattended-upgrades
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
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
crontab -e
*/5 * * * * /opt/n8n/watchdog.sh
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
Performance Tuning
For heavy workloads (1000+ executions/hour), add to docker-compose.yml:
n8n:
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 1G
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
SSL certificate issues
docker compose logs traefik | grep acme
# Check: DNS propagated? Port 80 accessible?
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)