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)
WEBHOOK_URL— the external address n8n writes into webhooks and OAuth callbacks. If it's still localhost, callbacks die in the webhook triangle and OAuth handshakes collide with redirect_uri_mismatch. Set it once, correctly, with scheme.N8N_HOST/N8N_PORT/N8N_PROTOCOL— the interface story the process tells about itself. Behind a proxy, align these with what the proxy expects; mismatches surface as editor URLs and redirects pointing nowhere.N8N_EDITOR_BASE_URL— when editor and API sit behind different hostnames, this keeps the UI honest.
Group 2 — Data & the key that guards everything
N8N_ENCRYPTION_KEY— the single most dangerous variable in the fleet. It encrypts every credential; lose or rotate it and your credential mountain becomes ciphertext rubble (the backup guide puts it in the restore set for exactly this reason). Pin it explicitly, store it like a master password, never let a container generate a fresh one silently.DB_TYPE+DB_POSTGRESDB_*— move off SQLite before scale writes its own ticket (the slowdown mechanism is documented in the performance guide); five vars: host, port, database, user, password.EXECUTIONS_DATA_PRUNE(defaulttrue) — the garbage collection you want on — verify someone didn't disable it during a debugging week.EXECUTIONS_DATA_MAX_AGE(default336hours) — how long history lives; size to your actual debugging horizon, not nostalgia.EXECUTIONS_DATA_PRUNE_MAX_COUNT(default10000) — the hard ceiling on stored executions; busy instances hit this before age limits.
Group 3 — Time & schedule (wrong zone = wrong hour)
GENERIC_TIMEZONE— the cron's clock. Unset silently inherits UTC's worldview; the full failure taxonomy (including the DST double-fire, tracked upstream as issue #42070) is in the cron & timezone guide.TZ— the container clock: logs, timestamps, your sanity when correlating events. Set both, deliberately, to an IANA name.
Group 4 — Performance ceilings
N8N_PAYLOAD_SIZE_MAX(default16MB) — webhook body ceiling; raising it isn't tuning, it's an invitation. Prefer chunking over big payloads.N8N_CONCURRENCY_PRODUCTION_LIMIT(default: uncapped) — caps simultaneous production executions so one burst doesn't tax everyone (burst mechanics: the memory guide).EXECUTIONS_TIMEOUT/EXECUTIONS_TIMEOUT_MAX— the kill switch by clock: a runaway execution should die to a timeout, not to your RAM. Cap the fleet maximum; tune per-workflow outliers on purpose.
Group 5 — Scale-out & files
EXECUTIONS_MODE=queue+QUEUE_BULL_REDIS_*— the doorway to worker fleets (full setup + traps: queue-mode guide).N8N_DEFAULT_BINARY_DATA_MODE(default/filesystem/s3) — where files live; queue mode demands shared storage ( binary-data guide).N8N_RESTRICT_FILE_ACCESS_TO— the allowlist fence for disk-reading nodes. Set it before you discover why it exists.
Group 6 — Operations & noise
N8N_LOG_LEVEL(error→debug) — incident-time verbosity dial; run atinfo/warnnormally, lower only while hunting.N8N_DIAGNOSTICS_ENABLED(falseto opt out) — telemetry governance explicit, set on purpose for compliance-sensitive deployments.N8N_SECURE_COOKIE/N8N_PROXY_HOPS— the security-hardening pair for proxy-fronted fleets (cookie semantics over TLS; trust exactly as many proxies as you run, no more).
Two more that complete the twenty (both security)
N8N_BLOCK_ENV_ACCESS_IN_NODE=true— stops Code nodes and expressions from reading process environment: the wall between "workflow author" and "server secrets". On any instance with more than one author, this is not optional.N8N_RUNNERS_ENABLED— task runners isolate Code-node execution from the main process (the modern hardening model; older single-process behavior assumed every Code node was trusted). Verify which model your installed version defaults to before inheriting an old compose file's assumption.
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
- Dump ground truth:
docker exec … env | grep -E 'N8N|GENERIC|DB_|EXECUTIONS|QUEUE'. - Check the five mandatory-production set exist and are intentional (webhook URL, key, zone, DB, pruning).
- Diff against version control — the live env and the repo file should tell one story.
- Confirm no secret lives in a committed file (history included).
- 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 timeThree 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).