// writing.automation

A Practical Guide to Self-Hosting n8n with Docker

n8n lets you orchestrate data and automate across tools. Self-hosting gives you control over data, cost, and extensibility, so you can tune performance, run custom code, and integrate private systems.

This step-by-step plan starts on Google Cloud’s free tier, then shows you how to put n8n behind HTTPS, keep data persistent, and upgrade safely.

1) Self-hosting n8n for free on Google Cloud

Google Cloud’s Always Free program includes one non-preemptible e2-micro VM per month in specific US regions, 30 GB-months of Standard persistent disk, and 1 GB of outbound data from North America per month. Eligible regions for the free e2-micro are us-west1 (Oregon), us-central1 (Iowa), us-east1 (South Carolina). The limit is by hours per month, combined across those regions.

Other hosting options: AWS, Hostinger, and DigitalOcean Droplets all work well. For this guide, we focus on Google Cloud’s free option to minimize cost.

Create the VM on Google Cloud

In the Google Cloud Console, create or select a project, enable Compute Engine, then click Create instance.

Use these settings to stay within free-tier limits: Region: us-west1, us-central1, or us-east1. Machine type: e2-micro. Boot disk: 30 GB Standard Persistent Disk. Firewall: check Allow HTTP and Allow HTTPS.

Reserve a static external IP, then assign it to the VM. Connect the IP with your domain’s A record in your registrar (for example n8n.example.com). SSH into the VM from the console to continue.

Advanced for tiny RAM: add a small swapfile

A modest swapfile can reduce out-of-memory crashes during spikes. It is not a replacement for RAM, it is a safety net.

# Create a 2G swapfile
# Allocate 2G Swapfile
sudo fallocate -l 2G /swapfile

# secure permissions and make it a swap area
sudo chmod 600 /swapfile
sudo mkswap -f /swapfile

# activate now
sudo swapon /swapfile

# verify
free -h
swapon --show
2) Docker, in plain English

Image: a packaged app plus all dependencies.

Container: a running instance of an image.

Volume: a folder on disk that survives restarts and upgrades.

3) Install Docker and test

Run these on the VM as a user with sudo.

# Patch the VM
sudo apt-get update && sudo apt-get upgrade -y

# Install Docker using the convenience script
curl -fsSL https://get.docker.com | sudo sh

# Allow your user to run docker without sudo (log out and back in after this)
sudo usermod -aG docker $USER

# Verify
docker run hello-world
docker version

# Install Docker Compose plugin
sudo apt-get install -y docker-compose-plugin
docker compose version
4) Quick start: n8n with a single docker run

Create folders and an environment file for persistent settings.

sudo mkdir -p /opt/n8n /opt/n8n-media
sudo chown -R $USER:$USER /opt/n8n /opt/n8n-media
nano /opt/n8n/.env

Paste this minimal .env, then save.

# External URL for webhooks and redirects
WEBHOOK_URL=https://n8n.example.com

# Timezone
GENERIC_TIMEZONE=America/Los_Angeles

# Execution and storage
NODE_ENV=production
EXECUTIONS_MODE=regular
EXECUTIONS_PROCESS=main
N8N_DEFAULT_BINARY_DATA_MODE=filesystem
N8N_BINARY_DATA_STORAGE_PATH=/media

Start n8n, bound to localhost so it is not exposed directly.

docker run -d   --name n8n   --restart unless-stopped   --env-file /opt/n8n/.env   -p 127.0.0.1:5678:5678   -v /opt/n8n:/home/node/.n8n   -v /opt/n8n-media:/media   n8nio/n8n:latest

Check that it is healthy.

docker ps
curl -s http://127.0.0.1:5678/healthz
5) Add HTTPS with Nginx and a free TLS certificate

Install Nginx and Certbot.

sudo apt-get install -y nginx certbot python3-certbot-nginx

Create an Nginx site file.

sudo tee /etc/nginx/sites-available/n8n.conf >/dev/null <<'NGINX'
server {
    listen 80;
    server_name n8n.example.com;

    location / {
        proxy_pass         http://127.0.0.1:5678/;
        proxy_set_header   Host $host;
        proxy_set_header   Origin $http_origin;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade  $http_upgrade;
        proxy_set_header   Connection $connection_upgrade;
    }
}
NGINX

Enable and test.

sudo ln -s /etc/nginx/sites-available/n8n.conf /etc/nginx/sites-enabled/n8n.conf
sudo nginx -t
sudo systemctl reload nginx

Get a certificate and auto-enable HTTPS.

sudo certbot --nginx -d n8n.example.com --redirect -m you@example.com --agree-tos -n

Update .env so n8n knows its external URL is HTTPS, then restart n8n.

WEBHOOK_URL=https://n8n.example.com

docker restart n8n
6) Docker Compose version for easier upgrades

Compose makes your setup repeatable.

mkdir -p /opt/n8n-compose && cd /opt/n8n-compose
cp /opt/n8n/.env ./.env
nano docker-compose.yml

docker-compose.yml:

services:
  n8n:
    image: n8nio/n8n:1.116.0
    container_name: n8n
    restart: unless-stopped
    ports:
      - "127.0.0.1:5678:5678"
    env_file:
      - .env
    volumes:
      - /opt/n8n:/home/node/.n8n
      - /opt/n8n-media:/media
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:5678/healthz"]
      interval: 30s
      timeout: 5s
      retries: 5

Start it.

docker compose up -d
docker compose ps

Upgrade by changing the image tag, then:

docker compose pull
docker compose up -d

Check that it is healthy.

docker ps
curl -s http://127.0.0.1:5678/healthz
7) Basics of self-hosted n8n: environment, storage, and backups
Environment variables you will actually use

WEBHOOK_URL must match your public HTTPS URL so webhooks and redirects work.

GENERIC_TIMEZONE sets timestamps in the UI and logs.

N8N_DEFAULT_BINARY_DATA_MODE=filesystem moves large payloads out of the DB and onto disk at N8N_BINARY_DATA_STORAGE_PATH.

Storage with SQLite

The official image uses SQLite by default, stored under /home/node/.n8n. Because you mapped /opt/n8n:/home/node/.n8n, your database and credentials persist across restarts and upgrades. SQLite is perfect for small to medium workloads and single node setups, then you can migrate to Postgres later when you need more concurrency.

Back up regularly.

# Briefly stop to get a clean snapshot
docker stop n8n
tar -czf /opt/backups/n8n-$(date +%F).tar.gz /opt/n8n /opt/n8n-media
docker start n8n
8) Troubleshooting

View logs: docker logs -f n8n

Health check: curl -s https://n8n.example.com/healthz

Free disk from old images: docker image prune -a and docker system prune -a

Patch the VM: sudo apt-get update && sudo apt-get upgrade -y

9) Updating n8n to the latest version
If you used a single docker run

Choose a specific version tag rather than latest, so you can roll back predictably. Replace X.Y.Z with the target version.

# Pull the new image
docker pull n8nio/n8n:X.Y.Z

# Stop and remove the running container
docker stop n8n && docker rm n8n

# Recreate with the same flags you used originally
docker run -d   --name n8n   --restart unless-stopped   --env-file /opt/n8n/.env   -p 127.0.0.1:5678:5678   -v /opt/n8n:/home/node/.n8n   -v /opt/n8n-media:/media   n8nio/n8n:X.Y.Z

# Verify
docker logs --since=5m n8n | tail -n +1

# Rollback if needed
docker stop n8n && docker rm n8n
docker run -d ... n8nio/n8n:<previous-tag>
If you used Docker Compose

Edit docker-compose.yml and set the new tag for the image line, for example:

image: n8nio/n8n:X.Y.Z

Apply the update with minimal downtime:

docker compose pull
docker compose up -d
docker compose ps

# Verify
curl -s http://127.0.0.1:5678/healthz
docker compose logs -f --since=5m

Rollback: restore the old tag in docker-compose.yml, then:

docker compose pull
docker compose up -d
If you also run Runners

Update the Runners image tag in the same docker-compose.yml, then:

docker compose pull runners
docker compose up -d runners
// back to all writing