← n8n Production Guides

REFERENCE · Operations

The 20 n8n Environment Variables That Actually Matter

2026-10-25 · William Ayad · 10 min read

The environment-variables documentation lists everything; production incidents are caused by about twenty of them. This is the opinionated map — grouped by what breaks, with the real defaults and the failure story each one prevents. Set them with intent, put them in version control next to your compose file, and verify the running process actually received them (the one-command check is in the ritual at the end).

Group 1 — Identity & reachability (wrong URL = silent dead webhooks)

Group 2 — Data & the key that guards everything

Group 3 — Time & schedule (wrong zone = wrong hour)

Group 4 — Performance ceilings

Group 5 — Scale-out & files

Group 6 — Operations & noise

Two more that complete the twenty (both security)

The mechanics nobody documents in the incident

Four rules cover 90% of "I set it and nothing changed": (1) env vars apply at process start — compose edits without up -d change nothing. (2) precedence runs shell > compose environment > .env import — know which flavor your deployment reads so two files don't argue. (3) quoting is load-bearing: values with #, spaces or $ break differently per file format. (4) verify the running process, not the file: docker exec <container> env | grep N8N is ground truth — the same ground-truth habit the scheduling guide demands for timezones.

Security: the two exposures

First: env vars are visible to anyone who can docker inspect or read your compose repo — never commit .env to git, ever; put secrets in a manager or sealed mount, with a committed .env.example naming the vars but not the values. Second: the encryption key's blast radius is 100% of credentials — a lost key invalidates every connection at once (the 401 map lists the symptoms you'd meet the morning after a silent regeneration: every integration, dead, simultaneously).

The ten-minute environment audit ritual

  1. Dump ground truth: docker exec … env | grep -E 'N8N|GENERIC|DB_|EXECUTIONS|QUEUE'.
  2. Check the five mandatory-production set exist and are intentional (webhook URL, key, zone, DB, pruning).
  3. Diff against version control — the live env and the repo file should tell one story.
  4. Confirm no secret lives in a committed file (history included).
  5. Log the audit date in the same ops calendar that holds your upgrade cadence.

A worked example: the compose stanza that survives audits

The shape teams converge on after their second incident — intentionally boring:

# docker-compose.yml (excerpt) — values come from .env, never inline secrets
services:
  n8n:
    image: n8nio/n8n:1.x.y        # pinned tag
    env_file: .env                 # not committed to git
    environment:
      - WEBHOOK_URL=https://flows.example.com/
      - GENERIC_TIMEZONE=Europe/Berlin
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}   # expanded from .env at compose time

Three lessons baked in: the pinned tag, the separation of stuff that's safe in the repo (variable names, structure) from stuff that never enters it (values), and expansion at compose time so the file in git narrates intent without leaking secrets.

The five veterans still forget

After every list-article read, the same five regressions recur in the field: WEBHOOK_URL never re-set on a moved replica; the encryption key missing from the disaster drill; GENERIC_TIMEZONE reverting to UTC after a region migration; EXECUTIONS_DATA_PRUNE_MAX_COUNT left default on a fleet that tripled volume; and N8N_RESTRICT_FILE_ACCESS_TO configured only after the first "whoops, a workflow read /etc" moment. The audit ritual above exists precisely because memory is not a control.

Reading the docs like an adult

Defaults drift between major versions — a value true in 1.x can flip in 2.x; an env var can deprecate in favor of a settings file entry. Before trusting any list (this one included), open the version-matched documentation for your installed release and sanity-check the handful you rely on most. Two minutes at upgrade time beats discovering a silently-renamed default in a postmortem.

The gotchas gallery (each one cost somebody a day)

Case-sensitivity: Linux env keys are exact — n8n_host isn't N8N_HOST. Whitespace: trailing spaces after values in .env files parse as content; a "correct" URL with a trailing space fails as if mistyped. CRLF: Windows-edited .env files can smuggle carriage returns that break parsing in invisible ways. Duplicates: when the same variable is defined in compose and .env-imported sections, precedence rules decide silently which value won. Each trap's defense is the same ground-truth command: read what the process received, never what you intended to send.

Governance: environment changes are code changes

The mature loop: infra edits go through the same review as application code (a PR that shows the env diff), every change lands with a one-line reason in the ops log, and the quarterly calendar holds the audit ritual beside the upgrade cadence and restore drill. Configuration invisible to process becomes configuration you rediscover during incidents; configuration under review becomes boring — and boring, in operations, is the highest available compliment.

The three-environment matrix (dev / staging / prod)

The variables above don't take one value — they take one value per environment, and the matrix is where mix-ups die harmlessly. A working template: WEBHOOK_URL differs per host (dev boxes get their own name, never the prod one — a dev instance answering prod callbacks is a story nobody laughs at twice); encryption keys differ (a stolen dump of dev credentials yields nothing about prod); timezone stays identical everywhere (time-zone per environment is how "works in staging" becomes a lie); pruning is generous in dev, strict in prod; queue mode and runners match production shape on staging, light on dev. Write the matrix as a three-column table in the repo next to the compose file; when a value changes, the row tells you which environments must change with it.

First-boot order of operations (the sequence that prevents déjà vu)

Day-one order matters because some variables are only safe before first data exists: encryption key pinned first (credentials created under an ephemeral key are grief), WEBHOOK_URL before the first external webhook is shown to anyone, timezone before the first schedule lives, Postgres details before first boot (retro-migrations off SQLite exist, but starting correct is free), pruning bounds while the executions table is still empty. Boot, verify with the ground-truth grep, run one webhook and one cron workflow as smoke — and only then invite the first real workflow aboard.

The checklist layer, if you want it pre-loaded

The n8n Reliability Kit (one-time $19, disclosing: mine) ships the twenty-var discipline as artifacts: the 34-point preflight checklist carries the environment section (mandatory-five presence, prune bounds, mode/zone sanity) with verification steps per item; the decision trees route "env set but nothing changed" and "wrong URL symptoms" to their mechanical causes; and the failure library keeps the encryption-key-loss and localhost-callback classes ranked by field incidence. Honest boundary, maintained as always: the kit audits exported workflows and operational discipline — it cannot (and nothing should) read your server environment remotely; the env | grep ground-truth step above is forever yours.

Frequently asked questions

Do env changes need a restart? Always — they're read at boot. Restart, then verify with docker exec … env; file contents are intent, process env is fact.

What if I lose N8N_ENCRYPTION_KEY? Every stored credential is permanently undecryptable. Back it up with the database; regenerate silently once and you'll meet a fleet-wide 401 morning.

Which vars are mandatory for production self-host? The effective five: WEBHOOK_URL, N8N_ENCRYPTION_KEY (pinned), GENERIC_TIMEZONE, DB_TYPE + DB_POSTGRESDB_*, pruning bounds — the rest are "set with intent" rather than discovered by incident.

Can Cloud users set these? No — Cloud manages the environment. This map is self-hosted territory.

Safest home for secrets in compose? Values live in an uncommitted .env or a mounted secrets store (and documented *_FILE conventions where your images support them) — never inline in committed YAML. The repo narrates intent; the vault keeps values.

The one-paragraph answer to bookmark

Twenty variables decide self-hosted weekends: set identity URLs right, pin the encryption key like the master secret it is, move to Postgres with explicit pruning bounds, pin the timezone pair, size payloads and concurrency ceilings deliberately, choose binary mode by fleet shape, fence file access, and govern logs and telemetry on purpose — then verify the running process, keep it all in version control, and audit it on a calendar entry instead of an adrenaline spike.

The environment audit, pre-loaded and repeatable

Checklist + decision trees + failure library — one-time $19, browser-only (disclosing: ours).