Skip to content
Cloudj

VPS use cases

n8n self hosting on a Frankfurt VPS: Docker, HTTPS and scaling

A practical guide to n8n self hosting on a Frankfurt VPS: Docker Compose with PostgreSQL, HTTPS via reverse proxy, backups, and when to switch to queue mode.

Updated · 4 min read

n8n self hosting puts your workflow automation, its credentials and its execution history on a server you control. For teams in the European Union, running it on a Frankfurt VPS also keeps that data stored in Germany. This guide is built around the order in which a real deployment grows: a single-container start, a production layout with PostgreSQL and HTTPS, routine backups, and finally queue mode when one process is no longer enough.

What "data in the EU" does and does not mean

Hosting n8n in Frankfurt means the workflow definitions, encrypted credentials and execution logs live on a disk in Germany. That simplifies conversations about where data is stored. It does not make every workflow stay in the EU: a node that calls a US-hosted API sends the payload there. Map which services your workflows touch, and treat hosting location as one part of your data-protection work, not the whole of it.

Cloudj's Frankfurt servers include 5 TB of traffic on monthly plans. If you are comparing German cities and neighbours, best VPS in Germany covers the location and the Europe VPS guide compares all seven European countries.

Sizing for n8n self hosting: start small

n8n's own process is modest. Memory use grows with the size of the data your workflows carry, such as large JSON responses, files and binary data. A practical path:

StageServerLayout
Trying it out1 vCPU, 2 GB RAMSingle container, SQLite
Team production2 vCPU, 4 GB RAMn8n plus PostgreSQL, reverse proxy
Heavy or many parallel workflows4 vCPU, 8 GB RAM or moreQueue mode with Redis and workers

Shared CPU is fine for most automation, which waits on APIs more than it computes. Move to general purpose or dedicated CPU if workflows transform large datasets. You can resize CPU and RAM later from the console, so pick the stage you are in today.

Production setup with Docker Compose

Start from a server with SSH keys, a firewall and Docker installed; the Docker install guide covers that. Point a DNS record such as n8n.example.com at the server's IP, then create a project folder:

mkdir -p ~/n8n && cd ~/n8n

Create .env with your values. Generate the encryption key once and store a copy somewhere safe: without it, saved credentials cannot be decrypted.

cat > .env <<'EOF'
N8N_HOST=n8n.example.com
POSTGRES_PASSWORD=change-me-long-random
EOF
echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env

Create docker-compose.yml:

services:
  postgres:
    image: postgres:16
    restart: always
    environment:
      - POSTGRES_USER=n8n
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=n8n
    volumes:
      - pg_data:/var/lib/postgresql/data

  n8n:
    image: docker.n8n.io/n8nio/n8n   # pin a version tag in production
    restart: always
    depends_on:
      - postgres
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
      - N8N_HOST=${N8N_HOST}
      - N8N_PROTOCOL=https
      - N8N_PORT=5678
      - WEBHOOK_URL=https://${N8N_HOST}/
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - GENERIC_TIMEZONE=Europe/Berlin
      - TZ=Europe/Berlin
    ports:
      - "127.0.0.1:5678:5678"
    volumes:
      - n8n_data:/home/node/.n8n

  caddy:
    image: caddy:2
    restart: always
    network_mode: host
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data

volumes:
  pg_data:
  n8n_data:
  caddy_data:

n8n listens only on 127.0.0.1, so the editor and webhooks are reachable only through the reverse proxy. Caddy obtains and renews a Let's Encrypt certificate automatically and passes WebSocket connections through, which the n8n editor needs. Create Caddyfile:

n8n.example.com {
    reverse_proxy 127.0.0.1:5678
}

Open the web ports and start everything:

sudo ufw allow 80,443/tcp
docker compose up -d
docker compose logs -f n8n

Visit https://n8n.example.com and create the owner account straight away, before anyone else finds the page. If you already run Nginx on the server, use it instead of Caddy; the Nginx reverse proxy and SSL guide shows the configuration, and remember to pass the Upgrade and Connection headers for WebSockets.

WEBHOOK_URL matters more than it looks. Without it, n8n may display webhook URLs with the internal port, and external services will call an address that does not work.

Backups and updates

Back up three things: the PostgreSQL database, the encryption key in .env, and an export of workflows as a readable second copy.

cd ~/n8n
docker compose exec -T postgres pg_dump -U n8n -Fc n8n > n8n-$(date +%F).dump
docker compose exec -T n8n mkdir -p /home/node/.n8n/backups
docker compose exec -T n8n n8n export:workflow --backup --output=/home/node/.n8n/backups/
docker compose cp n8n:/home/node/.n8n/backups ./workflow-backups

Copy the dump, the .env file and the workflow-backups folder off the server on a schedule. To update n8n, read the release notes, take a backup, then:

docker compose pull n8n
docker compose up -d

Pinning a version tag instead of using the default tag means updates happen when you decide, not whenever the container restarts.

Queue mode: when one process is not enough

In the default mode, a single n8n process serves the editor, receives webhooks and runs every execution. When long or numerous executions start slowing the editor or delaying webhooks, switch to queue mode. A Redis instance holds the queue, the main process accepts triggers, and separate worker containers run the executions.

The core changes are a Redis service and a few variables shared by the main process and the workers:

  redis:
    image: redis:7
    restart: always

  n8n-worker:
    image: docker.n8n.io/n8nio/n8n
    restart: always
    command: worker
    depends_on:
      - redis
      - postgres
    environment:
      - EXECUTIONS_MODE=queue
      - QUEUE_BULL_REDIS_HOST=redis
      # plus the same DB_* and N8N_ENCRYPTION_KEY values as the main n8n service

Add EXECUTIONS_MODE=queue and QUEUE_BULL_REDIS_HOST=redis to the main n8n service as well. Workers need the same encryption key and database settings, otherwise they cannot read credentials. Scale with docker compose up -d --scale n8n-worker=3, and size the server for the number of workers you run. Check the n8n documentation for the current options before you switch, since configuration details change between releases.

A Frankfurt server running n8n often grows into a small internal platform. A self-hosted Git server for automation scripts fits well next to it, and the Minecraft on a German VPS guide shows the same location used for a very different workload. Keep separate concerns on separate servers once any one of them becomes important.

FAQ

Can I use SQLite instead of PostgreSQL?

For testing and light personal use, yes. For a team, PostgreSQL handles concurrent executions and execution history more reliably and is easier to back up consistently.

Is self-hosted n8n free to use?

n8n is distributed under its own fair-code licence, which allows self-hosting for internal use. Read the licence terms if you plan to offer n8n to customers as a service.

Do I need queue mode from the start?

No. Start with the single-process layout and switch when executions begin to slow the editor or webhooks. The database stays the same.

What happens if I lose the encryption key?

Workflows survive, but stored credentials cannot be decrypted and must be entered again. Keep the key with your backups.

Run n8n in Frankfurt

Start with the Compose layout above on a small Frankfurt server, add backups on day one, and move to queue mode when the workload asks for it. Check live prices on the pricing page and deploy from the Cloudj console.

Deploy a server or order proxies

Pick a location in the console, see the live price, and pay from your balance.