Getting n8n running takes five minutes: docker run, open the editor, build a workflow. Keeping it running unattended — surviving reboots, redeploys, a lost server, a rotated secret — is a different job. This is the setup I run on a DigitalOcean droplet — n8n in Docker, behind nginx, sharing the box with this very site — for when n8n needs to be infrastructure, not a toy.
It assumes a single small VPS (2 vCPU / 4 GB is plenty to start) and Docker. The same shape scales up later without rework.
Need this built and handed over, not figured out on a deadline? That's what I do.
Use Postgres from day one
n8n defaults to SQLite. It's fine for a demo and a liability for anything you rely on — concurrent executions contend on a single file, and you can't move to queue mode later without migrating. Start on Postgres and skip the migration entirely.
Run both as one Compose stack, with the editor bound to localhost so only your reverse proxy faces the internet:
services:
n8n:
image: n8nio/n8n:1.70.0 # pin a version — never :latest
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678" # localhost only; the proxy faces the world
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- N8N_HOST=n8n.example.com
- N8N_PROTOCOL=https
- WEBHOOK_URL=https://n8n.example.com/
- GENERIC_TIMEZONE=Asia/Manila
volumes:
- n8n_data:/home/node/.n8n
depends_on: [postgres]
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=n8n
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=n8n
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
n8n_data:
pg_data:
restart: unless-stopped is what gets you back after a reboot. The named volumes are what survive a docker compose down. Secrets come from a .env file next to the compose file — never inline.
Put it behind HTTPS with a reverse proxy
n8n shouldn't terminate TLS itself. Bind it to 127.0.0.1 (above) and let a reverse proxy — nginx, Caddy, Traefik — handle HTTPS and forward to it. Caddy gets you a certificate with almost no config; nginx + Let's Encrypt is the same pattern this site runs on.
The part people miss is that n8n needs to know its public URL. N8N_HOST, N8N_PROTOCOL=https, and especially WEBHOOK_URL have to match the domain the proxy serves — otherwise the webhook URLs n8n hands to Stripe, Shopify, or GitHub point at the wrong place and silently never fire. Set those env vars and your webhooks register correctly the first time.
Set the encryption key yourself
n8n encrypts saved credentials with a key it auto-generates on first run. In Docker, that key can live inside an ephemeral layer — and if it changes, every stored credential becomes undecryptable. You'll be re-entering every API key by hand with no warning.
Generate one yourself and pin it:
openssl rand -hex 32 # put the result in .env as N8N_ENCRYPTION_KEY
Then store it in your password manager. This is the single most common way people lose a weekend with self-hosted n8n. Treat the key as more important than the database — you can rebuild workflows, you can't recover credentials without it.
Lock down access
Create the owner account the moment the editor loads — an open n8n instance is remote code execution waiting to happen (it runs arbitrary Code nodes). Beyond that: keep the editor off the public internet where you can (IP allowlist at the proxy, or a VPN), pull every secret from credentials and env vars rather than hard-coding them in nodes, and keep the VPS firewall down to 80/443/SSH. Public webhook endpoints don't require a public editor — separate the two.
Back up the two things that matter
Two, specifically: the Postgres database (workflows + execution history) and the encryption key. A nightly pg_dump shipped off-box covers the first; the second lives in your password manager. Test a restore once — a backup you've never restored is a hope, not a backup.
Pin the version, upgrade on purpose
:latest means an unrelated docker compose pull can upgrade n8n mid-incident. Pin a tag, read the release notes, bump it deliberately, and pg_dump first. n8n ships often and occasionally breaks node behavior between minors — you want upgrades to be a decision, not a surprise.
Scale only when you feel it
One n8n process handles a lot. When executions start queuing or a long workflow blocks others, switch EXECUTIONS_MODE=queue, add Redis, and run separate worker containers that share the same Postgres and encryption key. Don't reach for this on day one — it's the upgrade you grow into, and starting on Postgres is what makes it a config change instead of a rebuild.
The throughline
A production n8n install comes down to a handful of decisions that are cheap up front and expensive to retrofit: Postgres over SQLite, a known encryption key, HTTPS with the right public URL, and backups you've actually restored. Get those right and n8n stops being something you babysit — which is the same standard I bring to keeping automations reliable and the backend systems behind them.
If you'd rather have this built and handed over clean, let's talk.