n8n is a pleasure to self-host — a single container gets you running in minutes. The gap is between "running" and "production," and it is filled by a handful of decisions the quick-start skips: a proper database, a durable encryption key, TLS, and backups. Skip them and you have a demo that will lose data; make them and you have infrastructure you can trust.
Use Postgres, not SQLite
n8n defaults to SQLite, which is fine for trying it and wrong for production. Point it at PostgreSQL: it handles concurrent access, is required for queue mode, and is far easier to back up and restore reliably. This is the first change to make on any instance you intend to depend on.
Persist the encryption key and data
Two things must survive a container rebuild: the encryption key and the data. Set N8N_ENCRYPTION_KEY explicitly from a secret — if the container regenerates it, every stored credential becomes unreadable. And persist the data directory and database on volumes so a redeploy does not wipe your workflows and history.
services:
n8n:
image: n8nio/n8n
environment:
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY} # from a secret, pinned
N8N_HOST: n8n.example.com
N8N_PROTOCOL: https
WEBHOOK_URL: https://n8n.example.com/ # correct public webhook URLs
volumes: [ n8n_data:/home/node/.n8n ]
# front with a reverse proxy terminating TLS
TLS and correct webhook URLs
Put n8n behind a reverse proxy that terminates TLS — the editor and, critically, the webhook endpoints must be served over HTTPS. Set WEBHOOK_URL to the public HTTPS address so the webhook URLs n8n generates and hands to external providers are actually reachable. A mismatched webhook URL is the most common "my triggers don't fire" self-hosting problem.
Back up the Postgres database and the encryption key together, on a schedule, and store the key separately from the dump. Then actually test a restore — a database backup without the matching key cannot decrypt a single credential, and you do not want to learn that during a recovery. Restore drills are the only proof a backup works.
A production n8n is Postgres instead of SQLite, an explicit and safely-backed-up encryption key, persisted volumes, TLS with correct public webhook URLs, and — once load demands it — queue mode. None of it is hard; all of it is the difference between automation you can rely on and a container that loses your work the first time it restarts.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.