Self-hosting n8n on GCP: A production setup with Docker, PostgreSQL, and Nginx — cover image
Tutorial11 minMay 12, 2026
🏗️

Self-hosting n8n on GCP: A production setup with Docker, PostgreSQL, and Nginx

I replaced $500+/month in SaaS automation subscriptions with a self-hosted n8n instance on GCP. Here's the exact Docker + PostgreSQL + Nginx + SSL setup, plus the 3 mistakes I made so you don't have to.

B
Baraar Sreesha
Applied AI & GTM Systems Engineer

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:

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

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

yaml
# /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):

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

Step 5 — First boot and the 3 mistakes I made

Start the stack with the env vars in place:

bash
cd /opt/n8n
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .env
docker compose up -d
docker compose logs -f n8n  # watch the first boot

Visit 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:

  1. 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.
  2. 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.
  3. 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:

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

Cost breakdown

Here's what this setup actually costs per month, compared to equivalent SaaS plans:

$12.41/mo
Total: GCP e2-small VM + 30 GB disk + egress

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.

Want this for your team?

I build production AI systems — RAG, agents, GTM automation — for B2B teams worldwide. Book a 20-minute intro call.

Book a 20-min intro call