Self-Hosting n8n on a $5 VPS — A Production Setup That Actually Works
From bare Hetzner VPS to production-ready n8n in one sitting — Docker, PostgreSQL, nginx with SSL, automated backups and monitoring. The exact setup I use for clients.
n8n cloud starts at $20/month with execution limits. Self-hosted costs $5/month with no limits. But you can't just run docker run n8n and call it production — you need backups, SSL, restart policy, log rotation. This is the setup I've shipped for 12+ clients in the last two years.
What you'll need
- A Hetzner Cloud account (CX22 instance, ~$5/month)
- A domain name (free DNS on Cloudflare)
- Basic SSH skills
- One hour
1. Spin up the server
On Hetzner I provision a CX22 (2 vCPU, 4GB RAM, 40GB disk) running Ubuntu 24.04. Add an SSH key — password login is a bad idea anywhere on the internet. Once it's up I add an A record in Cloudflare: n8n.example.com → 1.2.3.4. I turn the orange cloud OFF initially so Let's Encrypt can verify.
First SSH and basic hardening
ssh [email protected]
# Sistem update
apt update && apt upgrade -y
# Firewall
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
# Yeni user yarat (root-da işləmə)
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
2. Install Docker and Compose
curl -fsSL https://get.docker.com | sh
usermod -aG docker deploy
# Docker compose plugin
apt install -y docker-compose-plugin
# Yoxla
docker --version
docker compose version3. Write the n8n docker-compose
Key tricks here: I add Postgres (SQLite is the default but slows down past 1000 workflows), set an n8n encryption key (encrypts stored credentials), and persist both n8n data and the database via named volumes.
# /home/deploy/n8n/docker-compose.yml
services:
postgres:
image: postgres:16
restart: always
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U n8n']
interval: 5s
n8n:
image: n8nio/n8n:latest
restart: always
depends_on:
postgres:
condition: service_healthy
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
N8N_HOST: n8n.example.com
N8N_PROTOCOL: https
WEBHOOK_URL: https://n8n.example.com/
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
GENERIC_TIMEZONE: Asia/Baku
ports:
- "127.0.0.1:5678:5678"
volumes:
- n8n_data:/home/node/.n8n
volumes:
postgres_data:
n8n_data:
# /home/deploy/n8n/.env
POSTGRES_PASSWORD=$(openssl rand -hex 24)
N8N_ENCRYPTION_KEY=$(openssl rand -hex 24)WARNING: don't commit .env to GitHub. If you lose that encryption key, every stored credential becomes unreadable. I keep mine in a 1Password vault with a separate backup copy.
4. nginx + Let's Encrypt SSL
apt install -y nginx certbot python3-certbot-nginx
# /etc/nginx/sites-available/n8n
cat > /etc/nginx/sites-available/n8n <<'EOF'
server {
listen 80;
server_name n8n.example.com;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Webhook-lar üçün
proxy_buffering off;
proxy_read_timeout 300s;
}
}
EOF
ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
# SSL almaq
certbot --nginx -d n8n.example.com --non-interactive --agree-tos -m sə[email protected]5. Start n8n
cd /home/deploy/n8n
docker compose up -d
# Logları izlə
docker compose logs -f n8nAfter a few seconds, hit https://n8n.example.com and create your admin user. The email/password it asks for is your n8n login — separate from any third-party credentials you'll add later for OpenAI, Slack, whatever.
6. Automated backups
Running production without backups is the fastest way to lose a job. I dump Postgres nightly, ship the dump to S3 (or Backblaze B2 which is cheaper), and keep 30 days of rolling history.
# /home/deploy/n8n/backup.sh
#!/bin/bash
set -e
DATE=$(date +%F)
BACKUP_DIR=/home/deploy/backups
mkdir -p $BACKUP_DIR
docker exec n8n-postgres-1 pg_dump -U n8n n8n | gzip > $BACKUP_DIR/n8n-$DATE.sql.gz
# n8n data folder-i də (workflow JSON-ları DB-də saxlanır, amma əlavə etmək zərər vermir)
tar -czf $BACKUP_DIR/n8n-data-$DATE.tar.gz -C /var/lib/docker/volumes/n8n_n8n_data/_data .
# S3-ə yüklə
aws s3 cp $BACKUP_DIR/n8n-$DATE.sql.gz s3://my-backups/n8n/
aws s3 cp $BACKUP_DIR/n8n-data-$DATE.tar.gz s3://my-backups/n8n/
# 30 gündən köhnəni sil
find $BACKUP_DIR -mtime +30 -delete
# Crontab: hər gecə 3:00-da işlə
# 0 3 * * * /home/deploy/n8n/backup.sh >> /var/log/n8n-backup.log 2>&17. Monitoring — know when it dies
UptimeRobot is free, pings every 5 minutes, and emails or Telegrams you when the site goes down. Set once, forget. For deeper monitoring you can run Grafana + Prometheus, but UptimeRobot handles the 90% case fine.
8. Turn Cloudflare proxy back on
Once SSL is sorted I turn the Cloudflare proxy (orange cloud) back on. Free DDoS protection, hides the real IP, caches static assets. Watch out for webhook issues — add a Page Rule: n8n.example.com/webhook/* → Cache Level: Bypass to stop Cloudflare from caching responses to your webhooks.
Real monthly cost
- Hetzner CX22 server: $5.18
- Domain (~$12/year): $1.00
- Backblaze B2 backup (10 GB): $0.06
- UptimeRobot: $0
- Cloudflare DNS: $0
- Total: ~$6.24/month
For comparison: n8n Cloud Pro starts at $50/month with a 10,000-execution cap. Self-hosted handles millions of executions per month without breaking a sweat. I've been running this exact setup at clients doing 50,000+ executions monthly for 2 years — uptime has been essentially perfect.
Need help on a project?
If something in this post hits close to a project you're working on, let's hop on a 30-minute call — I'll come back with concrete advice.