← All posts
Automation

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.

Fuad Aliyev June 15, 202612 min readAzərbaycanca oxu: Azərbaycanca

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

bash
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

bash
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 version

3. 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.

yaml
# /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:
bash
# /home/deploy/n8n/.env
POSTGRES_PASSWORD=$(openssl rand -hex 24)
N8N_ENCRYPTION_KEY=$(openssl rand -hex 24)
Note

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

bash
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

bash
cd /home/deploy/n8n
docker compose up -d

# Logları izlə
docker compose logs -f n8n

After 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.

bash
# /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>&1

7. 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.