Self-host n8n on Google Cloud for free: e2-micro, Docker & Traefik
Running your own n8n is the cheapest way to automate seriously: no per-execution pricing, no workflow limits, and your data stays on your server. Google Cloud's Always Free tier includes one e2-micro VM every month, which is enough to run n8n 24/7 — if you respect its 1 GB of RAM.
This guide is the setup I run in production: Docker Compose, Traefik v3 for automatic Let's Encrypt HTTPS, and the memory tuning that stops the kernel's OOM killer from freezing the machine.
TL;DR — e2-micro in a free-tier US region, 30 GB standard disk, 2 GB swap, Traefik + n8n in Docker with a 750 MB memory cap and aggressive execution pruning.
What you'll build
| Piece | Role |
|---|---|
GCP Compute Engine e2-micro |
2 shared vCPUs, 1 GB RAM, Ubuntu 24.04 LTS |
| Traefik v3 | Reverse proxy, HTTP→HTTPS redirect, automatic TLS certificates |
| n8n | The automation engine, capped in memory and pruning old executions |
| Swap file (2 GB) | Safety net so memory spikes slow down instead of crash |
Prerequisites
- A Google Cloud account with billing enabled (required even for free-tier resources).
- A domain you control — you'll point
n8n.yourdomain.comat the VM. - Basic comfort with SSH and a terminal.
Step 1 — Create the VM (and keep it free)
The free tier is strict about where and what. Get any of these wrong and you'll be billed.
In Compute Engine → VM instances → Create instance:
- Name:
n8n - Region:
us-west1,us-central1orus-east1— only these three qualify. - Machine type: General purpose → E2 →
e2-micro. - Boot disk: Ubuntu 24.04 LTS, Standard persistent disk, up to 30 GB. The default Balanced disk is not free — change it.
- Firewall: tick Allow HTTP traffic and Allow HTTPS traffic.
- Networking → External IPv4 address: reserve a static address so your DNS never breaks after a restart.
Click Create, then add a DNS A record for n8n.yourdomain.com pointing to that static IP. Do this now — certificate issuance in Step 5 needs DNS to resolve.
Cost check: free-tier rules (and the pricing of external IPv4 addresses) change from time to time. Create a budget alert in Billing → Budgets & alerts for €1/$1 so any surprise shows up in your inbox, not on your statement.
Step 2 — Add swap (the step everyone skips)
With 1 GB of RAM, Docker + Traefik + Node.js leave very little headroom. One workflow that downloads a large file and the kernel's OOM killer starts terminating processes — often leaving the VM unreachable until you reset it.
A 2 GB swap file turns those spikes into a brief slowdown instead. SSH into the VM (the SSH button in the console works fine) and run:
# Create and secure a 2 GB swap file
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# Keep it after reboots
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# Prefer RAM, use swap only under pressure
echo 'vm.swappiness=20' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl --system
# Verify
free -h
Step 3 — Install Docker
sudo apt update && sudo apt upgrade -y
# Official convenience script: Docker Engine + Compose plugin
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Run docker without sudo (takes effect in new shells)
sudo usermod -aG docker $USER
newgrp docker
docker --version && docker compose version
You don't need
ufwhere: GCP's VPC firewall already blocks everything except SSH, HTTP and HTTPS. Enablingufwwithout allowing port 22 first is a classic way to lock yourself out.
Step 4 — Configure n8n and Traefik
mkdir -p ~/n8n-docker/local-files
cd ~/n8n-docker
The .env file
nano .env
DOMAIN_NAME=yourdomain.com
SUBDOMAIN=n8n
SSL_EMAIL=you@example.com
GENERIC_TIMEZONE=Europe/Madrid
# Encrypts the credentials stored in n8n. Generate once with:
# openssl rand -hex 32
# and keep a copy somewhere safe — without it, a restored backup can't read its credentials.
N8N_ENCRYPTION_KEY=paste-your-generated-key-here
The docker-compose.yml
nano docker-compose.yml
services:
traefik:
# Traefik < 3.6.1 can't talk to Docker Engine 29+ (API version too old).
image: traefik:v3.6
restart: always
command:
- "--api=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.tlschallenge=true"
- "--certificatesresolvers.le.acme.email=${SSL_EMAIL}"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- traefik_data:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
deploy:
resources:
limits:
memory: 96M
n8n:
image: docker.n8n.io/n8nio/n8n:stable
restart: always
labels:
- traefik.enable=true
- traefik.http.routers.n8n.rule=Host(`${SUBDOMAIN}.${DOMAIN_NAME}`)
- traefik.http.routers.n8n.entrypoints=websecure
- traefik.http.routers.n8n.tls=true
- traefik.http.routers.n8n.tls.certresolver=le
- traefik.http.services.n8n.loadbalancer.server.port=5678
- traefik.http.middlewares.n8n-headers.headers.contentTypeNosniff=true
- traefik.http.middlewares.n8n-headers.headers.forceSTSHeader=true
- traefik.http.middlewares.n8n-headers.headers.stsSeconds=31536000
- traefik.http.middlewares.n8n-headers.headers.stsIncludeSubdomains=true
- traefik.http.routers.n8n.middlewares=n8n-headers@docker
environment:
- N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
- N8N_PORT=5678
- N8N_PROTOCOL=https
- NODE_ENV=production
- WEBHOOK_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- TZ=${GENERIC_TIMEZONE}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
# Memory tuning for a 1 GB machine
- NODE_OPTIONS=--max-old-space-size=512
- N8N_PAYLOAD_SIZE_MAX=16
- EXECUTIONS_DATA_PRUNE=true
- EXECUTIONS_DATA_MAX_AGE=48
- EXECUTIONS_DATA_PRUNE_MAX_COUNT=2000
- EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
- EXECUTIONS_DATA_SAVE_ON_ERROR=all
- EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=true
deploy:
resources:
limits:
memory: 750M
volumes:
- n8n_data:/home/node/.n8n
- ./local-files:/files
volumes:
n8n_data:
traefik_data:
Why these settings
Traefik
- TLS challenge: Traefik requests and renews Let's Encrypt certificates over port 443 by itself — no certbot, no cron.
- HTTP → HTTPS: everything arriving on port 80 is redirected.
- No exposed n8n port: port
5678is only reachable inside Docker's network; the internet only ever talks to Traefik. exposedbydefault=false: only containers withtraefik.enable=trueare published.
n8n
--max-old-space-size=512keeps the Node.js heap at a predictable size, so garbage collection runs before the container hits its limit.- Execution pruning (
EXECUTIONS_DATA_*) stops the SQLite database from filling with the payloads of successful runs. Failed runs are kept for 48 hours so you can still debug them. memory: 750Mis a hard cap: if n8n misbehaves, Docker restarts n8n — not the whole VM — and the OS and Traefik keep their share.N8N_ENCRYPTION_KEYset explicitly means your backups are portable. If n8n generates the key itself, it lives only inside the volume.:stabletag instead oflatestavoids pulling pre-releases. Pin an exact version (e.g.n8nio/n8n:1.x.y) if you want fully reproducible upgrades.
Step 5 — Launch
docker compose up -d
docker compose logs -f traefik # watch the certificate being issued, Ctrl+C to exit
Open https://n8n.yourdomain.com and create the owner account. Do it right away — until you do, anyone who finds the URL can claim the instance.
Keep an eye on it
docker stats # live CPU / RAM per container
docker compose logs -f n8n # n8n logs
free -h # RAM and swap usage
Step 6 — Backups
Everything that matters lives in the n8n_data volume. A daily tarball is enough for most personal setups:
mkdir -p ~/backups
docker run --rm \
-v n8n-docker_n8n_data:/data:ro \
-v ~/backups:/backup \
alpine tar czf /backup/n8n-$(date +%F).tgz -C /data .
Add it to crontab -e (for example 0 3 * * *) and copy the files off the VM from time to time. Also export important workflows as JSON (or sync them to Git) — the cheapest backup there is.
Troubleshooting
- Certificate not issued / browser warning: DNS must resolve to the VM before Traefik starts. Check with
dig +short n8n.yourdomain.com, thendocker compose restart traefik. client version 1.24 is too oldin Traefik logs: you're on a Traefik image older than 3.6.1 with Docker 29+. Usetraefik:v3.6or newer.- 404 from Traefik: the
Host(...)rule doesn't match the domain you typed — check.env. - VM freezes: confirm swap is active (
swapon --show) and look forKilled processinsudo dmesg. - Webhooks show
localhostURLs:WEBHOOK_URLis missing or wrong.
Upgrading
cd ~/n8n-docker
docker compose pull && docker compose up -d
docker image prune -f
Take a backup first, and read the n8n release notes before major-version jumps.
That's it: a production-grade n8n on hardware that costs nothing. If you build something with it, I'd love to see it — say hi through the contact form.