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:
| Stage | Server | Layout |
|---|---|---|
| Trying it out | 1 vCPU, 2 GB RAM | Single container, SQLite |
| Team production | 2 vCPU, 4 GB RAM | n8n plus PostgreSQL, reverse proxy |
| Heavy or many parallel workflows | 4 vCPU, 8 GB RAM or more | Queue 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.
Related setups on the same server
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.