automation · Deploy guide
Self-host n8n
Free and source-available fair-code licensed workflow automation tool. Here's how to run n8n on your own infrastructure — no marketing puffery, honest pitfalls, one recommended stack.
Every guide on SwapToSaaS runs on servers we actually pay for. n8n's footprint fits comfortably on this class of instance. Different provider preference? The steps below work anywhere Docker runs.
Provision a server →Before you start
n8n is a workflow-automation tool with 400+ integrations. This guide gets you from an empty server to a working, TLS-protected n8n instance in about 45 minutes, on a €7/month Hetzner CX32. The CX32 (8 GB RAM) matters — n8n's Node.js process spikes memory when webhooks bunch up, and a smaller box will OOM under real load.
You'll need:
- A domain you control (e.g.
n8n.yourcompany.com) with DNS pointing to your server's IPv4. - Basic terminal skills. Nothing fancy.
- An email address for Let's Encrypt to send expiry notices to.
Step 1 · Provision the server (5 min)
Create a new Hetzner Cloud project, then a CX32 instance in the datacenter closest to your users. Ubuntu 24.04 LTS. Add your SSH public key. Once it boots, note the IPv4 address.
ssh root@YOUR_SERVER_IP First move: create a non-root user with sudo and disable password login.
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart ssh Log out and log back in as deploy. Everything from here runs as that user with sudo.
Step 2 · Point DNS to the server (5 min)
In your DNS provider (Cloudflare, Namecheap, whatever), create an A record:
n8n.yourcompany.com → YOUR_SERVER_IP If you're on Cloudflare, disable the orange cloud proxy for the initial cert issuance. You can re-enable proxying after Let's Encrypt succeeds. Otherwise HTTP-01 challenges fail.
Wait 2-3 minutes and verify: dig +short n8n.yourcompany.com returns your server IP.
Step 3 · Install Docker and Docker Compose (5 min)
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker Step 4 · Write the docker-compose stack (10 min)
Create a directory and drop this docker-compose.yml into it. It runs n8n behind Caddy (for automatic HTTPS) with a Postgres backend for durability.
mkdir -p ~/n8n && cd ~/n8n
mkdir -p caddy_data caddy_config n8n_data postgres_data ~/n8n/docker-compose.yml:
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
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 -d n8n"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: n8nio/n8n:latest
restart: unless-stopped
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_HOST}
N8N_PROTOCOL: https
N8N_PORT: 5678
WEBHOOK_URL: https://${N8N_HOST}/
GENERIC_TIMEZONE: UTC
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
volumes:
- ./n8n_data:/home/node/.n8n
expose:
- "5678"
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy_data:/data
- ./caddy_config:/config
depends_on:
- n8n
~/n8n/Caddyfile:
${N8N_HOST} {
reverse_proxy n8n:5678
} ~/n8n/.env (fill in real values):
N8N_HOST=n8n.yourcompany.com
POSTGRES_PASSWORD=REPLACE_WITH_32_CHAR_RANDOM_STRING
N8N_ENCRYPTION_KEY=REPLACE_WITH_ANOTHER_32_CHAR_STRING Generate strong passwords: openssl rand -base64 32.
Step 5 · Bring it up (5 min)
cd ~/n8n
docker compose up -d
docker compose logs -f Watch the logs. Caddy will fetch a Let's Encrypt certificate for your domain. When you see Server started on port 5678 and Caddy reports certificate obtained successfully, hit Ctrl+C to stop tailing logs.
Open https://n8n.yourcompany.com in your browser. First-time setup will ask for owner email and password. Set them.
Step 6 · Firewall and backups (15 min, don't skip)
Firewall — only ports 22, 80, 443 open to the world:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable Backups — nightly Postgres dump to a separate location:
sudo tee /etc/cron.daily/n8n-backup > /dev/null <<'EOF'
#!/bin/bash
set -e
BACKUP_DIR=/home/deploy/backups
mkdir -p "$BACKUP_DIR"
docker exec n8n-postgres-1 pg_dump -U n8n n8n | gzip > "$BACKUP_DIR/n8n-$(date +%Y%m%d).sql.gz"
find "$BACKUP_DIR" -name "n8n-*.sql.gz" -mtime +14 -delete
EOF
sudo chmod +x /etc/cron.daily/n8n-backup Copy those backups off-server to S3, Backblaze B2, or wherever you like. Nightly. This is the difference between "I self-host n8n" and "I lost 6 months of workflows."
Common pitfalls
- Webhooks not firing. Almost always because
WEBHOOK_URLin .env doesn't match what the outside world can reach. Verify withcurl https://n8n.yourcompany.com/webhook/test-path. - OOMs at low traffic. The default Docker memory settings sometimes cap Node.js to 512 MB. Add
NODE_OPTIONS: "--max-old-space-size=2048"to the n8n environment block if you see OOM kills indocker compose logs. - TLS cert not issuing. Cloudflare orange-cloud proxy blocks HTTP-01 challenges. Disable proxying (grey cloud) for the initial issuance, then re-enable.
- Executions accumulating forever. By default n8n keeps every execution record. Set
EXECUTIONS_DATA_MAX_AGE: 168(hours) to auto-prune older than 7 days.
Updating n8n
cd ~/n8n
docker compose pull
docker compose up -d
docker image prune -f n8n releases nearly weekly. Update every 2-4 weeks. Read the release notes first — occasional migrations require attention.
What you're paying vs Zapier
- n8n on Hetzner CX32: €85/year (~$92) + your time on updates.
- Zapier Team (20k tasks/mo): $800+/year, and it scales linearly.
- n8n Cloud Starter (official): $240/year — worth it if you want managed but want to escape Zapier pricing.
Rough rule: 2-3 hours per month of maintenance. If that's worth $700/year to you, keep self-hosting. Below that, use n8n Cloud.
Hosts that work well for n8n
Some links are affiliate. We only include hosts we deploy production workloads on. Rankings above are independent of these links.