You’ve read about n8n. You’ve maybe tried the cloud version. Now you want to run it on your own server, where you control the data, skip the execution limits, and pay a fraction of the cloud price.
This guide walks through the entire process: picking a server, installing n8n with Docker Compose, setting up a reverse proxy with SSL, configuring backups, and keeping everything updated. I’ve done this setup for clients and for my own workflows, so I’ll include the things that took me time to figure out.
First, figure out if self-hosting is right for you
Self-hosting n8n is not for everyone. Here’s a quick decision framework.
Self-host if:
- You run more than 2,500 executions per month (the n8n Cloud Starter limit)
- You need your data to stay in a specific region (GDPR, client contracts)
- You’re comfortable with a Linux terminal, or willing to learn
- You want to use external npm packages in your code nodes
Use n8n Cloud if:
- You just want it to work without thinking about servers
- You need SSO, Git-based version control, or audit logs
- You don’t have anyone on the team who can SSH into a server
- You’re running fewer than 2,500 workflows per month
The cloud Starter plan costs €24/month for 2,500 executions. A self-hosted instance costs €4 to 6/month and has no execution limits at all. I covered the full pricing breakdown in my n8n pricing guide.
Step 1: Pick a VPS
n8n needs surprisingly little computing power. A 2 vCPU, 4GB RAM server handles 10 to 20 workflows with room to spare. Based on community benchmarks on a 2 vCPU server, n8n uses about 860MB of RAM at idle and adds 40 to 50MB per concurrent execution.
Here’s what the main providers charge for a server that can comfortably run n8n (prices as of April 2026):
| Provider | Plan | Monthly |
|---|---|---|
| Hetzner | CX23: 2 vCPU, 4GB, 40GB NVMe | €3.99 |
| Hetzner (ARM) | CAX11: 2 vCPU, 4GB, 40GB NVMe | €3.79 |
| Contabo | VPS S: 4 vCPU, 8GB, 100GB NVMe | €4.50 |
| DigitalOcean | Basic: 2 vCPU, 4GB, 80GB SSD | ~€22 |
I recommend Hetzner CX23 for most people. It’s cheap, fast, and the data centers are in Germany (Falkenstein and Nuremberg) and Finland. If you serve European clients, this matters for GDPR: your automation data stays in the EU, on servers operated by a German company subject to German data protection law.
Contabo gives you more specs per dollar, but Hetzner has better network performance and a cleaner management interface. DigitalOcean is a solid choice if you prefer a US-based provider, but you’ll pay about 6x more for equivalent specs.
ARM works fine. Hetzner's CAX11 (ARM) is about 5% cheaper than CX23 (x86). n8n's Docker image supports both architectures (amd64 and arm64). The only caveat: some community nodes that bundle native binaries might not have ARM builds. For standard integrations (Shopify, Google Sheets, Slack, Xero), ARM runs identically.
Why hosting location matters
If you use n8n Cloud, your data is stored in Frankfurt (EU). That’s fine for most cases. But self-hosting gives you more control: you choose the exact data center, you know the data never leaves that machine, and you can prove it to clients or auditors.
For European businesses, this is more than a nice-to-have. GDPR requires that you can demonstrate where personal data is processed and stored. When a workflow pulls customer emails from Shopify, reformats them, and pushes them to your CRM, that data passes through your n8n instance. If that instance is on a Hetzner server in Nuremberg, you have a clear, auditable answer: “Customer data is processed on infrastructure operated by Hetzner Online GmbH in Nuremberg, Germany.”
If your clients are in the EU, or you handle EU customer data, hosting on a European server simplifies your compliance story. It’s not the only way to comply with GDPR, but it’s the easiest.
Step 2: Install Docker and Docker Compose
SSH into your new server and install Docker. On Ubuntu (the most common choice for VPS):
# Update packages
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com | sh
# Add your user to the docker group
sudo usermod -aG docker $USER
# Log out and back in, then verify
docker --version
Docker Compose comes bundled with modern Docker installations. Verify it works:
docker compose version
Step 3: Create the Docker Compose file
Create a directory for your n8n setup and add the compose file:
mkdir -p ~/n8n && cd ~/n8n
Here’s the docker-compose.yml I use for production deployments. It runs n8n with PostgreSQL (not SQLite) and Caddy as the reverse proxy with automatic SSL:
version: "3.8"
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
n8n:
image: n8nio/n8n:2.15.1
restart: unless-stopped
environment:
- N8N_HOST=n8n.yourdomain.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.yourdomain.com/
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=your-strong-password-here
- N8N_ENCRYPTION_KEY=your-random-encryption-key
- GENERIC_TIMEZONE=Europe/Berlin
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16
restart: unless-stopped
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=your-strong-password-here
- POSTGRES_DB=n8n
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U n8n"]
interval: 5s
timeout: 5s
retries: 10
volumes:
caddy_data:
caddy_config:
n8n_data:
postgres_data:
A few important details in this config:
- Pinned n8n version (
n8nio/n8n:2.15.1instead oflatest). n8n releases frequently, and some releases have introduced regressions (v1.99.1 had a confirmed memory leak). Pin the version and update deliberately. - PostgreSQL healthcheck. Without it, n8n sometimes starts before the database is ready and crashes on first connection. The
depends_onwithcondition: service_healthywaits for PostgreSQL to actually accept connections. - No exposed ports on n8n. The n8n container isn’t directly accessible from the internet. Only Caddy exposes ports 80 and 443, and proxies traffic to n8n internally. This means you can’t accidentally bypass HTTPS.
And the Caddyfile (create this in the same directory):
n8n.yourdomain.com {
reverse_proxy n8n:5678
}
Replace n8n.yourdomain.com with your actual domain, and generate real passwords. For the encryption key, run:
openssl rand -hex 32
Why Caddy over Nginx or Traefik. Caddy handles SSL certificates automatically. No Certbot cron jobs, no renewal scripts, no manual configuration. It requests a Let's Encrypt certificate the first time someone hits your domain, renews it automatically, and redirects HTTP to HTTPS by default. For a single-service setup like n8n, Caddy is the simplest option. Traefik is better if you're running many services with dynamic routing. Nginx is better if you need fine-grained control over headers and caching.
SQLite vs PostgreSQL: which database to use
n8n defaults to SQLite. It works fine for testing, but I always use PostgreSQL for production. Here’s why:
- Concurrent access. SQLite locks the entire database during writes. When two workflows trigger at the same time, one waits. PostgreSQL handles concurrent writes natively.
- Execution log growth. n8n stores execution logs in the database. After a few months of active use, the SQLite file can grow to several gigabytes. PostgreSQL handles large datasets more gracefully and lets you query logs without slowing down the main application.
- Backup flexibility. PostgreSQL has
pg_dump, which creates consistent, point-in-time backups without stopping n8n. SQLite backups require either stopping the container or risking a corrupted copy.
The performance difference is negligible for small setups (under 5 workflows). But if you’re going to grow, starting with PostgreSQL saves you from a migration later.
Step 4: Point your domain and start
Before starting, point a DNS A record for your subdomain (e.g. n8n.yourdomain.com) to your server’s IP address. Then:
cd ~/n8n
docker compose up -d
Caddy will automatically request an SSL certificate. Give it a minute, then open https://n8n.yourdomain.com in your browser. You should see the n8n setup screen asking you to create an owner account.
If something isn’t working, check the logs:
# All services
docker compose logs
# Just n8n
docker compose logs n8n
# Follow logs in real time
docker compose logs -f n8n
Step 5: Environment variables worth setting
The compose file above covers the basics. Here are a few more environment variables I set on every production instance:
# Prune execution logs older than 7 days (168 hours)
# Pruning is ON by default since n8n 1.x, but the default
# retention is 14 days. For busy instances, tighten it.
- EXECUTIONS_DATA_MAX_AGE=168
# Save only failed executions (saves disk space)
# Change to "all" if you need to debug workflows
- EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
# Enable external npm modules in code nodes
- NODE_FUNCTION_ALLOW_EXTERNAL=*
# Prevent code nodes from reading your env vars
- N8N_BLOCK_ENV_ACCESS_IN_NODE=true
# Limit concurrent workflow executions
- N8N_CONCURRENCY_PRODUCTION_LIMIT=20
The execution pruning matters more than people think. Even with pruning enabled, the database can grow fast if you save successful executions. One community member reduced their database from 1.5GB to 153MB just by switching EXECUTIONS_DATA_SAVE_ON_SUCCESS to none. If you need to debug a specific workflow, you can always turn saving back on temporarily.
Note: pruning deletes rows, but PostgreSQL doesn’t automatically reclaim the disk space. If you notice the database size isn’t shrinking after enabling pruning, run VACUUM FULL on the database once (or set up an automated job for it).
Step 6: Set up backups
This is the step most people skip, and the one that matters most when something goes wrong. There are two things to back up: the PostgreSQL database and the n8n data volume (which contains your encryption key and credential files).
Database backup (daily cron job):
#!/bin/bash
# Save as ~/n8n/backup.sh and chmod +x
BACKUP_DIR=~/n8n/backups
mkdir -p $BACKUP_DIR
docker compose exec -T postgres \
pg_dump -U n8n -d n8n \
| gzip > "$BACKUP_DIR/n8n-$(date +%Y%m%d).sql.gz"
# Keep only last 14 days
find $BACKUP_DIR -name "*.sql.gz" -mtime +14 -delete
Add it to cron:
crontab -e
# Add this line:
0 3 * * * cd ~/n8n && ./backup.sh
Server snapshots. Most VPS providers offer automated snapshots for €1 to 3/month. On Hetzner, it’s 20% of the server price (about €0.80/month for a CX23). Enable it. This is your “everything went wrong” safety net.
Off-site copy. For critical setups, copy the daily database dump to a different location. An S3-compatible bucket (Hetzner Object Storage, Backblaze B2) costs pennies and means a dead server doesn’t take your backups with it.
The encryption key is sacred. n8n encrypts all stored credentials (API keys, OAuth tokens, passwords) with the N8N_ENCRYPTION_KEY you set in the compose file. If you lose this key, you lose access to every credential in your instance. Store it somewhere safe outside the server: a password manager, a printed copy, anything. Restoring a database backup without the matching encryption key means rebuilding every credential from scratch.
Step 7: Keep n8n updated
Updating is straightforward. Pull the new image, recreate the container, done:
cd ~/n8n
# Pull the latest image
docker compose pull n8n
# Recreate the container (keeps your data)
docker compose up -d n8n
Your workflows, credentials, and settings are stored in the PostgreSQL database and the data volume. The n8n container itself is stateless. Pulling a new image and restarting it applies the update without losing anything.
How often to update: I check for updates every two to four weeks. n8n releases frequently (sometimes weekly), but not every release is relevant to every user. Read the changelog before updating. Major version bumps (1.x to 2.x) sometimes include breaking changes to specific nodes.
Before updating: Run your backup script first. If an update breaks something, you can roll back:
# Roll back to a specific version
docker compose down n8n
# Edit docker-compose.yml: change the version tag to the previous one
docker compose up -d n8n
What a small VPS can actually handle
Community benchmarks on a 2 vCPU, 4 to 8GB RAM server show:
- Idle: ~860MB RAM, 0% CPU
- Per execution: +40 to 50MB RAM, brief CPU spike
- 3 concurrent executions: 17 to 27% CPU, ~900MB total RAM
- External API calls (Google Sheets, Shopify) add network overhead but minimal CPU/RAM
In practice, a €4/month Hetzner CX23 (2 vCPU, 4GB RAM) comfortably runs 10 to 20 active workflows that fire multiple times per day. That’s Shopify order syncs, invoice processing, CRM updates, Slack notifications, reporting. All running in parallel, no queue, no throttling.
If you outgrow that, the next step up is a CX33 (€6.49/month, 4 vCPU, 8GB RAM), which handles 30 to 50+ active workflows.
You don’t need queue mode unless you’re running hundreds of workflows. Queue mode (which adds Redis and a separate worker process) actually doubles idle RAM consumption to about 1.7GB. For most small businesses, it’s overkill.
The full cost breakdown
Here’s what a production n8n setup actually costs per month:
| Item | Cost | Notes |
|---|---|---|
| Hetzner CX23 | €3.99/mo | 2 vCPU, 4GB RAM, 40GB NVMe |
| Automated snapshots | €0.80/mo | 20% of server price |
| Domain | ~€1/mo | ~€12/year for a .com |
| SSL certificate | €0 | Let's Encrypt via Caddy |
| n8n license | €0 | Community Edition |
| Total | ~€5.80/mo | Unlimited everything |
Compare that to n8n Cloud at €24 to €60/month with execution caps, or Zapier at €20 to €70/month with per-task billing. Over a year, self-hosting saves €200 to €650 depending on what you’d otherwise pay.
The hidden cost is your time. Setup takes an afternoon. After that, maintenance is about 1 to 2 hours per month: checking for updates, reviewing logs, occasionally restarting a stuck workflow. If your time is worth €50/hour, that’s €50 to €100/month in labor. For some people, that math makes the cloud version a better deal. For others (especially if you enjoy this stuff, or if you have someone on the team who handles servers anyway), the savings and control are worth it.
Things that trip people up
I’ve set up n8n instances for clients and helped people troubleshoot their own. These are the most common issues:
Forgetting to set WEBHOOK_URL. Without this, n8n generates webhook URLs using the internal container hostname, which external services can’t reach. Set it to your public URL: https://n8n.yourdomain.com/.
Volume permissions. n8n runs as user node (UID 1000) inside the container. If you mount a host directory instead of a Docker volume, the permissions need to match: chown -R 1000:1000 ./n8n-data. Mismatched permissions cause silent write failures or crashes on startup.
Using latest tag in production. n8n releases frequently, sometimes weekly. Some releases have introduced memory leaks (v1.99.1 was a confirmed regression). Pin your Docker image to a specific version like n8nio/n8n:2.15.1 and update intentionally after reading the changelog.
Losing the encryption key. n8n encrypts all stored credentials (API keys, OAuth tokens, passwords) with the N8N_ENCRYPTION_KEY. If you redeploy with a new key (or forget to set one), every credential becomes permanently unrecoverable. No recovery mechanism exists. Write it down in a password manager before you start adding credentials.
Exposing port 5678 directly. If your compose file maps 5678:5678 instead of routing through a reverse proxy, n8n is accessible on the public internet without HTTPS. Worse, it bypasses any auth you’ve set up on the proxy. The compose file in this guide avoids this by only exposing ports 80 and 443 through Caddy.
OAuth configuration pain. Setting up Google OAuth (for Sheets, Gmail, Calendar) requires creating a project in Google Cloud Console, configuring the consent screen, and adding redirect URIs. It takes 10 to 20 minutes and feels tedious every time. Not a Docker issue, but it surprises people who expect a simple API key.
After you’re up and running
Once n8n is running on your server, the next step is building workflows. If you’re new to n8n, my beginner’s guide covers the basics: what nodes are, how triggers work, and what kinds of tasks make sense to automate first.
If you want to understand how self-hosting compares to cloud pricing in more detail, I wrote a full n8n pricing breakdown that covers cloud tiers, self-hosted costs, and the comparison with Zapier and Make.
And if you’d rather not manage the server yourself, that’s something I help with. I set up and maintain n8n instances for small businesses as part of the automation work I do at MPStudio. You get the cost savings and data control of self-hosting without having to think about Docker or cron jobs. Send me an email if that sounds useful.