After three years of paying for Zapier, Make, and a couple of niche automation SaaS tools, my monthly bill crossed $500. The workloads were not even that complex — most of them were webhook → CRM → Slack flows that any reasonable automation platform could handle. The problem was that every additional step or transfer cost more money, and the bills scaled linearly with usage. So I decided to self-host n8n on Google Cloud Platform. This guide walks through the exact setup, including the three mistakes I made so you can avoid them.
Why self-host n8n?
n8n's fair-code license lets you self-host unlimited workflows, executions, and integrations for free. You pay only for the VM (a $12/mo e2-small is plenty for a single team). For agencies and revenue teams running dozens of workflows, the math is obvious.
What you'll need
- A Google Cloud Platform account (free $300 credit on signup, valid 90 days)
- A registered domain (I use Namecheap — ~$10/year)
- Basic comfort with the terminal and SSH
- 30 minutes of focused time
Step 1 — Spin up a GCP VM
From the GCP Console, navigate to Compute Engine → VM Instances → Create. I recommend the following configuration for a small team (up to 20 active workflows):
- Machine type: e2-small (2 vCPU, 2 GB RAM) — $12/mo
- Boot disk: Debian 12 (Bookworm), 30 GB standard persistent disk
- Allow HTTP and HTTPS traffic in the firewall rules
- Static external IP: reserve one under VPC Network → External IP addresses
Mistake #1 I made
I initially picked an e2-micro (1 GB RAM) and n8n crashed every time a workflow processed a large webhook payload. The 2 GB on e2-small is the safe minimum. Don't skimp here.
Step 2 — Install Docker and Docker Compose
SSH into the VM and install Docker the official way:
# Update system
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com | sudo sh
# Add your user to the docker group so you don't need sudo
sudo usermod -aG docker $USER
# Log out and back in for the group change to take effect
exit
# SSH back in, then verify
docker --version
docker compose versionStep 3 — Create the Docker Compose stack
Create a directory for n8n and add a docker-compose.yml. The key decisions here are: (1) use PostgreSQL instead of the default SQLite for production durability, (2) pin the n8n version so updates are explicit, (3) mount a persistent volume for files n8n writes (like exports and binary data).
# /opt/n8n/docker-compose.yml
version: "3.8"
services:
postgres:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: n8nio/n8n:1.62.0
restart: always
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_HOST: n8n.yourdomain.com
N8N_PORT: 5678
N8N_PROTOCOL: https
WEBHOOK_URL: https://n8n.yourdomain.com/
N8N_EDITOR_BASE_URL: https://n8n.yourdomain.com/
GENERIC_TIMEZONE: Asia/Kolkata
N8N_METRICS: "true"
N8N_DIAGNOSTICS_ENABLED: "false"
N8N_RUNNERS_ENABLED: "true"
volumes:
- ./data/n8n:/home/node/.n8n
ports:
- "127.0.0.1:5678:5678" # Only listen on localhost — Nginx will proxy
volumes:
postgres:
n8n:Why 127.0.0.1:5678?
Binding n8n to localhost means it is NOT directly exposed to the internet. Nginx (which we'll add next) handles TLS termination and reverse-proxies to n8n. This is a critical security pattern — never expose n8n directly on a public port.
Step 4 — Add Nginx + free SSL via Let's Encrypt
Install Nginx and Certbot on the host (not in Docker — this keeps SSL certificate management simpler):
sudo apt install -y nginx certbot python3-certbot-nginx
# Create the Nginx config
sudo tee /etc/nginx/sites-available/n8n <<'EOF'
server {
server_name n8n.yourdomain.com;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# Issue the SSL cert (make sure your DNS A record points to the VM first)
sudo certbot --nginx -d n8n.yourdomain.com --redirect --agree-tos -m you@example.comStep 5 — First boot and the 3 mistakes I made
Start the stack with the env vars in place:
cd /opt/n8n
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .env
docker compose up -d
docker compose logs -f n8n # watch the first bootVisit https://n8n.yourdomain.com and you should see the n8n setup screen. Create your admin account, and you're live. Here are the three mistakes I made on my first attempt — each of them cost me hours:
- 1I forgot to set WEBHOOK_URL to the public HTTPS URL. Webhooks from external services (Stripe, Calendly, HubSpot) silently failed because n8n registered them with the internal Docker hostname. Always set WEBHOOK_URL to the public URL.
- 2I used the default SQLite backend and lost a week of workflow history when the container restarted unexpectedly. PostgreSQL takes 5 extra minutes to set up and gives you proper durability, backups, and the ability to migrate.
- 3I didn't set GENERIC_TIMEZONE. Scheduled triggers fired at the wrong hour because n8n defaulted to UTC and my CRM expected IST. Set it once at the env level — it applies to every workflow.
Step 6 — Backups
n8n stores credentials, workflow definitions, and execution history in PostgreSQL. Add a nightly backup cron that dumps the DB to a timestamped file and uploads to a Cloud Storage bucket:
# /opt/n8n/backup.sh
#!/bin/bash
set -e
TS=$(date +%Y%m%d-%H%M%S)
docker compose -f /opt/n8n/docker-compose.yml exec -T postgres \
pg_dump -U n8n n8n | gzip > /tmp/n8n-backup-$TS.sql.gz
gsutil cp /tmp/n8n-backup-$TS.sql.gz gs://your-bucket/n8n-backups/
find /tmp -name "n8n-backup-*.sql.gz" -mtime +7 -delete
# Cron: 0 2 * * * /opt/n8n/backup.sh >> /var/log/n8n-backup.log 2>&1Cost breakdown
Here's what this setup actually costs per month, compared to equivalent SaaS plans:
Compare that to: Zapier Team ($69/mo for 50k tasks), Make Core ($29/mo for 10k ops), plus a niche scraper tool I was paying $99/mo for. Total replaced: ~$500/mo. Payback period: less than one week.
When NOT to self-host
Self-hosting is not the right answer for everyone. You should NOT self-host n8n if:
- You don't have anyone on the team comfortable with Linux + Docker. The maintenance overhead will eat the savings.
- Your workflows process HIPAA, PCI, or SOC2-regulated data and you don't already have a hardened cloud posture. Use n8n Cloud or a managed equivalent instead.
- Your team has fewer than 3 active workflows — the setup time is not worth it for occasional automations.
What's next
Once n8n is running, the next move is to wire it into your GTM stack. My most-used workflows are: (1) Clay → n8n → HubSpot lead routing, (2) Calendly → n8n → Slack alert + CRM enrichment, and (3) a daily n8n job that scans a Google Sheet of target accounts and runs them through Apollo + an LLM for personalization. I'll write a follow-up post on the GTM stack specifically — for now, get n8n running and start moving one workflow at a time off the SaaS bill.
Want me to set this up for you?
I build GTM automation systems for founder-led B2B SaaS, agencies, and SMBs. If you want this exact stack set up and integrated with your CRM in 2 weeks, book a 20-minute intro call from the contact section below.
