Automation · n8n

Self-Hosting n8n in Production

Self-hosting n8n is straightforward to start and easy to do fragilely. A production setup means a real database, a safe encryption key, TLS, and backups you have tested.

John Kihiu12 min read

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.

YAML · a production-shaped n8n service
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.

A backup you haven't restored is a hope

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.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.