Install n8n on a VPS: Docker Compose + HTTPS (Step-by-Step)
Self-hosting runs the same core n8n product on infrastructure you control, with the free Community Edition unlocked by default (Business and Enterprise features unlock via a license key). The docs recommend Docker for most self-hosted setups because it wraps up all the dependencies and makes database and environment management much simpler.
By the end of this guide you will have a working, HTTPS-secured n8n instance on a single VPS that is ready for real workflows.
Prerequisites
Before you start, have these ready:
- A Linux VPS (Ubuntu or Debian). A comfortable production baseline is 2 vCPU / 4 GB RAM / 40+ GB SSD. A 1 vCPU / 1–2 GB box works for light personal workflows — add swap at 1 GB so a busy run does not get OOM-killed.
- Root or sudo access — you need it for Docker and firewall configuration.
- A domain or subdomain you control, e.g.
n8n.example.com. - Basic command-line and SSH comfort.
Why the specs matter: n8n itself is not CPU-hungry. Memory, storage, and any heavy jobs you run (video processing, large HTTP payloads, external binaries) are the real constraints, so size for your workload rather than for n8n alone.
1. Provision the VPS and point your domain
Spin up a VPS with at least 2 vCPU / 4 GB RAM and SSD storage in a region close to your users, then add a DNS A record for your subdomain pointing at the VPS public IP.
Why this comes first: DNS has to resolve to your server before Let’s Encrypt can issue a certificate later, and region choice affects both latency and where your data physically lives (GDPR). DigitalOcean, Linode/Akamai and Vultr are common global picks; Hetzner is a frequent EU choice and is called out in the official Docker Compose guides. There is no single “best” provider — any reputable 2 vCPU / 4 GB SSD box in your region is fine.
n8n.example.com. A 203.0.113.10 ; A record -> your VPS public IP
2. Harden the server
Create a non-root sudo user, set up SSH key login, optionally disable password logins, and lock the firewall down to only what you need: SSH, HTTP (80) and HTTPS (443).
Why: you are about to expose ports 80 and 443 to the internet, but n8n’s own port 5678 should stay closed — it will only listen on 127.0.0.1 inside the box. A small firewall footprint is your first line of defense.
sudo ufw allow OpenSSH # keep your SSH session alive sudo ufw allow 80/tcp # HTTP (used for the TLS challenge + redirect) sudo ufw allow 443/tcp # HTTPS sudo ufw enable sudo ufw status # confirm 5678 is NOT listed
Tip: Allow OpenSSH before running ufw enable, or you can lock yourself out of the server.
3. Install Docker and Docker Compose
Install Docker Engine and the Compose plugin using the official Docker instructions for your distro, then verify the install.
Why Docker: the docs recommend it for most self-hosted setups because it encapsulates dependencies and keeps database and environment management simple. Other official methods exist (npm, cloud provider images), but Compose on a single VPS is the most maintainable production pattern.
docker compose version # should print a version, e.g. Docker Compose v2.x
4. Create the project folder and .env secrets
Make a working directory and generate strong random secrets for the Postgres password and the n8n encryption key, then store them in a .env file that your Compose stack will read.
Why this matters a lot: N8N_ENCRYPTION_KEY encrypts the credentials n8n stores. Treat it and your database password as real secrets, and never lose the encryption key — without it you cannot decrypt saved credentials after a rebuild.
mkdir -p /opt/n8n && cd /opt/n8n # generate two strong random values openssl rand -hex 24 # -> POSTGRES_PASSWORD openssl rand -hex 24 # -> N8N_ENCRYPTION_KEY cat > .env <<EOF POSTGRES_USER=n8n POSTGRES_PASSWORD=paste_first_random_value POSTGRES_DB=n8n N8N_ENCRYPTION_KEY=paste_second_random_value EOF
5. Write docker-compose.yml
Define two services on a private Docker network with named volumes: a PostgreSQL container for persistence, and the n8n container itself. The key safety detail is binding n8n to 127.0.0.1:5678 so the raw port is never reachable from the internet.
What to notice: the Postgres pg_isready healthcheck means n8n only starts once the database is ready, and WEBHOOK_URL must match your real public HTTPS endpoint so the URLs and webhooks n8n generates line up with what sits behind your proxy.
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck: # n8n waits until the DB is ready
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks: [internal]
n8n:
image: docker.n8n.io/n8nio/n8n:latest # pin a version in production
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678" # localhost ONLY, never 0.0.0.0
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_HOST=n8n.example.com
- N8N_PORT=5678
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.example.com/ # note the trailing slash
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- GENERIC_TIMEZONE=Europe/Berlin
- TZ=Europe/Berlin
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
- NODE_ENV=production
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
networks: [internal]
volumes:
pg_data:
n8n_data:
networks:
internal:
Tip: Pin an explicit image tag (e.g. n8nio/n8n:1.XX.X) in production instead of latest, so an docker compose pull never surprises you with an unplanned major upgrade.
6. Bring the stack up
Start both containers in the background, confirm they are healthy, and tail the n8n logs on the first run so you can spot any startup errors early.
docker compose up -d docker compose ps # both services should show healthy/running docker compose logs -f n8n # watch first-run output, Ctrl+C to stop tailing
7. Set up the reverse proxy and HTTPS
Install a reverse proxy on the host (Nginx, Caddy or Traefik) that terminates TLS and forwards traffic to 127.0.0.1:5678. The n8n editor relies on WebSockets, so the proxy must pass the Upgrade and Connection headers through — miss these and you get a flaky or “unreachable” editor.
Certificates: a common pattern is Let’s Encrypt via Certbot with Nginx, then letting the certbot timer auto-renew. Caddy is popular precisely because it handles TLS automatically. Renewal automation is not optional — broken renewals are a classic “it worked for months then died” failure.
# --- Nginx server block (443) ---
server {
listen 443 ssl;
server_name n8n.example.com;
client_max_body_size 16m; # raise for large payloads/uploads
location / {
proxy_pass http://127.0.0.1:5678;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket support
proxy_set_header Connection "upgrade"; # WebSocket support
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# --- issue + auto-renew the certificate ---
sudo certbot --nginx -d n8n.example.com
sudo systemctl enable --now certbot.timer # automatic renewalTip: Prefer less config? Caddy will fetch and renew the certificate for you from a two-line site block that reverse-proxies to 127.0.0.1:5678.
8. Test n8n and WebSockets
Open https://n8n.example.com, complete the initial owner setup, and confirm three things: the UI is responsive, webhooks fire, and the browser console shows no WebSocket errors.
Why check the console: WebSocket errors here almost always trace back to missing proxy headers or aggressive CDN/Cloudflare caching — catching them now saves you a confusing debugging session later.
9. Set up backups and updates
Back up regularly and update deliberately. Dump the Postgres database on a schedule (a pg_dump cron job or a small backup container) and back up the n8n data volume too. For upgrades, always take a backup first, then pull and restart.
# scheduled DB backup (run via cron) docker compose exec -T postgres pg_dump -U n8n n8n > /opt/n8n/backup_$(date +%F).sql # update flow (take a backup BEFORE this) docker compose pull n8n docker compose up -d docker compose logs -f n8n # confirm a clean restart
VPS sizing at a glance
| Use case | vCPU | RAM | Storage | Notes |
|---|---|---|---|---|
| Minimal / personal | 1 | 1–2 GB | ~25 GB SSD | Add swap at 1 GB to avoid OOM kills |
| Production baseline | 2 | 4 GB | 40+ GB SSD | Handles concurrency, heavier payloads, DB growth |
| Heavier / ffmpeg | — | 16 GB | — | Community suggestion, unverified — reported for ffmpeg-heavy stacks; treat as anecdotal |
Common mistakes (and how to dodge them)
| Mistake | Why it hurts | Fix |
|---|---|---|
| Running on a 1 GB VPS with concurrent executions | Out-of-memory kills and crashes | Swap helps; upgrading to 2–4 GB is the reliable fix |
| Serving n8n over plain HTTP on port 5678 | Insecure and publicly exposed | Terminate TLS at a proxy; keep 5678 on 127.0.0.1 |
| Neglecting Let’s Encrypt renewal | Access and webhooks silently break after months | Automate renewal (certbot timer / Caddy ACME) |
| Wrong WEBHOOK_URL (http, missing slash, wrong host) | 404s and webhooks that never fire | Match the real public HTTPS hostname, include the trailing slash |
| Missing WebSocket headers / bad Cloudflare caching | Flaky editor, “editor unreachable” errors | Add Upgrade/Connection headers; check Cloudflare proxy & caching |
| Reaching for Kubernetes on day one | Needless complexity for a single instance | Start with single-VPS Docker Compose; scale only once you outgrow it |
Quick recap
- 2 vCPU / 4 GB / SSD is the comfortable production baseline.
- Run n8n + PostgreSQL in Docker Compose — the maintainable single-VPS pattern.
- Bind n8n to
127.0.0.1:5678, never0.0.0.0in production. - Put Nginx or Caddy in front for HTTPS and WebSocket headers.
- Set
WEBHOOK_URLto your real public HTTPS URL, with the trailing slash. - Auto-renew certificates and back up both Postgres and the n8n volume.
Frequently asked questions
What VPS specs do I need to run n8n?
A comfortable production baseline is 2 vCPU, 4 GB RAM and 40+ GB SSD. A 1 vCPU / 1–2 GB box is enough for light personal workflows, but add swap at 1 GB so heavy runs don’t get OOM-killed. n8n itself is modest on CPU — size for memory, storage and any heavy jobs you run.
Do I have to use Docker to self-host n8n?
No. Official install methods include npm, Docker, Docker Compose and several cloud-provider images. The docs recommend Docker for most self-hosted setups because it encapsulates dependencies and simplifies database and environment management, which is why Compose on a single VPS is the common production pattern.
Why shouldn't I expose port 5678 directly?
Serving n8n over plain HTTP on port 5678 is insecure and not recommended. Bind n8n to 127.0.0.1, terminate HTTPS at a reverse proxy, and open only ports 80, 443 and SSH to the internet.
Why are my webhooks or editor not working?
The usual causes are an incorrect WEBHOOK_URL (using http, a missing trailing slash, or the wrong hostname) which causes 404s and webhooks that don’t fire, or missing WebSocket headers / aggressive Cloudflare caching, which makes the editor flaky or unreachable. Fix the URL and make sure the proxy passes the Upgrade and Connection headers.

Written by
Marco Sansalone
Also in n8n Guide
Something Not Working? Tell Us What’s Wrong.
You'll be notified when the tutorial goes live and join our newsletter on AI tools and tutorials.
