Originally published at noderecipe.com.
Self-hosting n8n gives you unlimited workflow executions, full ownership of your data, and no per-task fees — all for the cost of a small server (about $5/month). This guide takes you from an empty VPS to a secured, HTTPS-enabled n8n instance running 24/7, using Docker and Docker Compose. You'll also get the environment variables that actually matter, a backup routine, and fixes for the errors that trip up most beginners.
No prior Docker experience is assumed. If you can copy a command into a terminal, you can finish this in about 30 minutes.
Self-hosted vs. n8n Cloud: which do you need?
Before you spin up a server, be honest about which path fits you. n8n offers a paid cloud plan and a free, source-available self-hosted option (the Community Edition). Here's the trade-off:
| Factor | Self-hosted (Community) | n8n Cloud |
|---|---|---|
| Price | ~$5/mo server, unlimited executions | From ~$24/mo, metered executions |
| Data location | Your server, your control | n8n's managed infrastructure |
| Maintenance | You handle updates & backups | Fully managed |
| Setup time | ~30 min (this guide) | Minutes |
| Custom nodes / npm packages | Full control | Limited |
| Best for | Developers, high-volume, privacy needs | Non-technical users, teams who want zero ops |
Choose self-hosting if you run a lot of executions, want your data on infrastructure you control, or need custom community nodes. Choose Cloud if you never want to think about servers — the honest version is that if your automations make you money and your time is worth more than $20/month, managed hosting is the cheaper option however it looks on the invoice. The rest of this guide covers self-hosting.
What you'll need
- A VPS (virtual private server) — roughly $5/month. See the comparison below.
- SSH access to that server (a terminal on Mac/Linux, or a client like PuTTY/Windows Terminal on Windows).
- A domain or subdomain you can point at the server (e.g.
n8n.yourdomain.com). This is required for HTTPS and for webhooks to work reliably. - About 30 minutes.
You do not need to know Docker beforehand — every command is provided.
Where to host n8n (VPS comparison)
Any Linux server with 1 GB of RAM will run a small n8n instance, but 2 GB is more comfortable once you have a few active workflows. These are the popular, budget-friendly options:
| Provider | Entry price | RAM | Best for |
|---|---|---|---|
| Hetzner Cloud | ~€4.5/mo | 2–4 GB | Best price-to-performance (EU/US regions) |
| DigitalOcean | $6/mo | 1 GB | Beginner-friendly UI, great docs |
| Railway | Usage-based | Scales | Fastest setup, no server management |
| Home server / Raspberry Pi | $0 | Varies | Tinkering, LAN-only automations |
For most people starting out, Hetzner's CX22 (2 GB RAM) or a DigitalOcean basic droplet is the sweet spot. Pick Ubuntu 24.04 LTS as the operating system when you create the server.
Two notes on picking between them, since this is the one decision that costs money. Hetzner is roughly half the price for double the RAM — a CX22 gives you 2 GB for about €4.5, where DigitalOcean's $6 droplet gives you 1 GB. If you are comfortable in a terminal, Hetzner is the better machine for the money. DigitalOcean is worth the premium if you are new to servers: the control panel is friendlier, and their documentation is genuinely the best in the business for the exact moment when something breaks at 11pm. Both give new accounts a signup credit that covers your first month or two.
Quick start: run n8n in one command
Want to see n8n before committing to the full setup? SSH into your server and run:
docker volume create n8n_data
docker run -d --restart unless-stopped --name n8n \
-p 5678:5678 \
-v n8n_data:/home/node/.n8n \
docker.n8n.io/n8nio/n8n
Open http://your-server-ip:5678 in your browser and create your owner account. That's a working n8n — but it has no HTTPS, no database, and no domain, so webhooks and OAuth logins will misbehave. Treat this as a test drive, then move to the production setup below.
Don't have Docker yet? Install it in one line:
curl -fsSL https://get.docker.com | sh
Production setup with Docker Compose
Docker Compose lets you define n8n, a PostgreSQL database, and a reverse proxy in a single file so they start together and survive reboots. This is the setup you actually want to keep.
Step 1: Install Docker and Compose
On a fresh Ubuntu server, run:
curl -fsSL https://get.docker.com | sh
The Compose plugin ships with modern Docker. Verify both:
docker --version
docker compose version
Step 2: Point your domain at the server
In your DNS provider (Cloudflare, Namecheap, etc.), create an A record for n8n.yourdomain.com pointing to your server's public IP. DNS can take a few minutes to propagate — you can continue while it does.
Step 3: Create the project folder
mkdir -p ~/n8n && cd ~/n8n
Step 4: Create the .env file
This holds your secrets and settings. Create ~/n8n/.env:
# --- Your domain ---
DOMAIN_NAME=n8n.yourdomain.com
# --- Database (Postgres) ---
POSTGRES_USER=n8n
POSTGRES_PASSWORD=change-this-to-a-long-random-string
POSTGRES_DB=n8n
# --- n8n security ---
# Generate with: openssl rand -hex 32
N8N_ENCRYPTION_KEY=paste-a-32-byte-random-hex-string-here
GENERIC_TIMEZONE=America/New_York
Generate strong values before saving:
openssl rand -hex 32 # use for N8N_ENCRYPTION_KEY
openssl rand -hex 16 # use for POSTGRES_PASSWORD
Keep
N8N_ENCRYPTION_KEYsafe. It encrypts all your saved credentials. If you lose it, every stored credential becomes unreadable and you'll have to re-enter them.
Step 5: Create docker-compose.yml
Create ~/n8n/docker-compose.yml:
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
n8n:
image: docker.n8n.io/n8nio/n8n:latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_HOST=${DOMAIN_NAME}
- N8N_PROTOCOL=https
- N8N_PORT=5678
- WEBHOOK_URL=https://${DOMAIN_NAME}/
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
- N8N_RUNNERS_ENABLED=true
volumes:
- n8n_data:/home/node/.n8n
expose:
- 5678
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
depends_on:
- n8n
volumes:
postgres_data:
n8n_data:
caddy_data:
caddy_config:
Note that n8n uses expose (internal only) rather than publishing port 5678 to the public internet — Caddy is the only service that faces the outside world.
The environment variables that matter
Most self-hosting problems come from missing or wrong environment variables. These are the ones you should always set on a production instance:
| Variable | Why it matters |
|---|---|
N8N_ENCRYPTION_KEY |
Encrypts stored credentials. Set it explicitly and back it up. |
N8N_HOST / WEBHOOK_URL
|
Must match your real domain, or webhooks and OAuth callbacks break. |
N8N_PROTOCOL=https |
Tells n8n it's served over HTTPS (behind the proxy). |
DB_TYPE=postgresdb |
Uses Postgres instead of the default SQLite — required for reliability at scale. |
GENERIC_TIMEZONE |
Makes Schedule/Cron nodes fire at the times you expect. |
N8N_RUNNERS_ENABLED=true |
Enables task runners, the recommended way to execute Code nodes. Required on n8n 1.x; deprecated from version 2.0, where it's on by default and can be omitted. |
One more worth knowing about if you import workflows you didn't write: N8N_BLOCK_ENV_ACCESS_IN_NODE defaults to false, which means Code nodes and expressions can read your environment variables — including the encryption key and database password above. Setting it to true closes that off. Importing templates safely covers why that matters and what else to check before running someone else's workflow.
Adding HTTPS with Caddy
Caddy provisions and renews a free Let's Encrypt certificate automatically — no manual certbot steps. Create ~/n8n/Caddyfile:
n8n.yourdomain.com {
reverse_proxy n8n:5678
}
Replace n8n.yourdomain.com with your real subdomain (it must match DOMAIN_NAME). That's the entire config — Caddy handles the TLS certificate the first time someone hits the domain.
Now start everything:
cd ~/n8n
docker compose up -d
Give it a minute (the certificate is issued on first request), then open https://n8n.yourdomain.com. Create your owner account and you're live — with a valid HTTPS padlock.
Check that all three containers are healthy:
docker compose ps
Keeping n8n running, updated, and backed up
Auto-restart: restart: unless-stopped in the Compose file means Docker brings your containers back after a crash or server reboot. Nothing else needed.
Updating n8n: pull the newest image and recreate the container. Your data lives in named volumes, so it survives the upgrade:
cd ~/n8n
docker compose pull
docker compose up -d
Backups — do not skip this. Your workflows and credentials live in Postgres and the n8n volume. Back up the database regularly:
docker compose exec -T postgres \
pg_dump -U n8n n8n > ~/n8n-backup-$(date +%F).sql
Copy that .sql file off the server (to your machine, S3, or a backup service) and keep your .env alongside it — remember, without N8N_ENCRYPTION_KEY the backup's credentials can't be decrypted. Automate it with a daily cron job for peace of mind.
Securing your instance
A public n8n instance is a target. Lock it down:
-
Firewall: allow only ports 22 (SSH), 80, and 443. On Ubuntu:
ufw allow 22 && ufw allow 80 && ufw allow 443 && ufw enable. - Never expose port 5678 directly. Let Caddy terminate TLS; keep n8n internal (as configured above).
- Use strong, unique secrets for the database password and encryption key.
-
Keep the server updated:
apt update && apt upgrade -yperiodically. - Restrict SSH to key-based auth and disable root password login.
Common errors and fixes
"This site can't provide a secure connection" / no HTTPS.
DNS isn't pointing at your server yet, or port 80/443 is blocked. Confirm the A record resolves (dig n8n.yourdomain.com) and that your firewall/cloud security group allows 80 and 443. Caddy needs port 80 reachable to issue the certificate.
Webhooks return the wrong URL or don't fire.
WEBHOOK_URL and N8N_HOST must match your public HTTPS domain exactly. If they still show localhost or an IP, you edited the values but didn't recreate the container — run docker compose up -d again.
"Command 'code' is not allowed" or Code node fails.
On n8n 1.x, enable task runners with N8N_RUNNERS_ENABLED=true (already in the Compose file above) and restart. On 2.0 and later, task runners are enabled by default and that variable is deprecated — if the error persists there, check the container logs instead.
Credentials show as "unable to decrypt."
The N8N_ENCRYPTION_KEY changed between runs. It must stay identical to the one used when the credentials were saved. Restore the original key from your backup.
Containers keep restarting.
Check logs to see which one: docker compose logs n8n --tail=50 or docker compose logs postgres --tail=50. The most common cause is a Postgres password mismatch after editing .env without recreating the database volume.
Schedule/Cron node fires at the wrong time.
Set GENERIC_TIMEZONE to your IANA timezone (e.g. Europe/London) and restart.
Frequently asked questions
Is self-hosting n8n free?
The n8n Community Edition is free to self-host, permanently and with no execution limit. Your only cost is the server (~$5/month) and your domain. One caveat worth knowing before you build a business on it: n8n is source-available, not open source, and its Sustainable Use License restricts hosting n8n for paying users. What's free and what isn't breaks that down, including what it means for client work.
Do I need a domain?
For a real setup, yes. Webhooks, OAuth logins, and HTTPS all depend on a proper domain. You can test on a raw IP, but production needs a domain. Google's OAuth flow makes this concrete: the redirect URI is registered against one fixed address, so connecting a service like Google Sheets means re-registering it every time your instance moves.
SQLite or Postgres?
n8n defaults to SQLite, which is fine for testing. For anything you rely on, use Postgres (as in this guide) — it handles concurrent executions far more reliably.
How much RAM do I need?
1 GB runs a light instance; 2 GB is comfortable for regular use with several active workflows. Heavy AI or data workflows benefit from more.
How do I move from n8n Cloud to self-hosted?
Export your workflows as JSON from Cloud and import them into your self-hosted instance, then re-enter credentials. Your workflows are portable JSON either way.
Ready to build? The docker-compose.yml and Caddyfile above are ready to copy as they stand. Once the instance is up, our templates page has eight importable workflows to run on it — error handling, monitoring, and AI starters. New to containers, or want the details on volumes, image tags, and safe updates? Start with the n8n Docker setup guide.
Top comments (0)