← All posts

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

  1. A Google Cloud account with billing enabled (required even for free-tier resources).
  2. A domain you control — you'll point n8n.yourdomain.com at the VM.
  3. 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-central1 or us-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 ufw here: GCP's VPC firewall already blocks everything except SSH, HTTP and HTTPS. Enabling ufw without 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 5678 is only reachable inside Docker's network; the internet only ever talks to Traefik.
  • exposedbydefault=false: only containers with traefik.enable=true are published.

n8n

  • --max-old-space-size=512 keeps 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: 750M is a hard cap: if n8n misbehaves, Docker restarts n8n — not the whole VM — and the OS and Traefik keep their share.
  • N8N_ENCRYPTION_KEY set explicitly means your backups are portable. If n8n generates the key itself, it lives only inside the volume.
  • :stable tag instead of latest avoids 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, then docker compose restart traefik.
  • client version 1.24 is too old in Traefik logs: you're on a Traefik image older than 3.6.1 with Docker 29+. Use traefik:v3.6 or 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 for Killed process in sudo dmesg.
  • Webhooks show localhost URLs: WEBHOOK_URL is 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.