FlowKit

n8n environment variables you should know (and how to use them)

Published 27 July 2026 · 7 min read

A self-hosted n8n instance is configured almost entirely through environment variables: the public URL, the database, the timezone, execution retention, credential encryption, queue mode. The official documentation lists dozens of them — but in practice, about fifteen cover the vast majority of needs, and two or three classic mistakes (an unsaved encryption key, a missing WEBHOOK_URL behind a reverse proxy, the default timezone) account for a good share of the support threads you'll find on the forums. This guide groups the genuinely useful variables by theme, shows how to set them cleanly with Docker, and how to access them from a workflow.

How to set an environment variable (self-hosted only)

With a Docker installation, there are two complementary approaches. The first: declare variables directly in the environment block of the n8n service in docker-compose.yml:

services:
  n8n:
    image: docker.n8n.io/n8nio/n8n
    environment:
      - GENERIC_TIMEZONE=Europe/Paris
      - TZ=Europe/Paris
      - WEBHOOK_URL=https://n8n.mydomain.com/

The second, preferable as soon as secrets are involved: put the values in a .env file next to docker-compose.yml (Docker Compose reads it automatically) and reference them with the ${...} syntax:

    environment:
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - DB_POSTGRESDB_PASSWORD=${DB_POSTGRESDB_PASSWORD}

The .env file must never be committed to a Git repository — add it to .gitignore on day one. And in every case, a container restart is required (docker compose up -d is enough): n8n reads its environment variables at startup, never on the fly.

On n8n Cloud, none of this is accessible: the infrastructure is managed by n8n, and you can neither define your own variables nor change the instance's. It's one of the structural differences between Cloud and self-hosted — if your requirements involve fine-grained configuration (binary data storage mode, queue mode, timeouts), self-hosting is the only route.

Instance and URLs

The theme that causes the most confusion, especially behind a reverse proxy:

Variable Role
N8N_HOST Hostname n8n considers itself reachable on
N8N_PORT Port the n8n process listens on (5678 by default)
N8N_PROTOCOL http or https, used to build URLs
WEBHOOK_URL Full public URL used to generate webhook URLs
N8N_EDITOR_BASE_URL Public URL of the editor, used in links generated by n8n

The classic trap: an instance behind Traefik or Caddy with HTTPS and a domain name that displays webhook URLs as http://localhost:5678/.... The n8n container doesn't know a reverse proxy publishes it at https://n8n.mydomain.com — telling it is precisely what WEBHOOK_URL is for. Without it, every webhook shown in the editor (and handed to third-party services) points to an address unreachable from the outside.

Database

By default, n8n uses SQLite — fine for testing, fragile in production. Switching to PostgreSQL is done entirely through variables:

Variable Role
DB_TYPE sqlite by default; postgresdb for PostgreSQL
DB_POSTGRESDB_HOST PostgreSQL server host
DB_POSTGRESDB_PORT Port (5432 by default)
DB_POSTGRESDB_DATABASE Database name
DB_POSTGRESDB_USER User
DB_POSTGRESDB_PASSWORD Password (put it in the .env, never in plain text in the compose file)

Mind the switchover: changing DB_TYPE on an existing instance does not migrate the data — workflows and credentials stay in the old database. The SQLite-to-PostgreSQL migration and the backup strategy that goes with it deserve a plan, detailed in our PostgreSQL backup and restore guide.

Timezone

Two variables, two distinct roles:

Variable Role
GENERIC_TIMEZONE Timezone used by n8n itself, notably by the Schedule Trigger
TZ System timezone inside the container (log timestamps, system commands)

Without GENERIC_TIMEZONE, a Schedule Trigger set to "every day at 9am" won't fire at 9am your local time — n8n applies its default timezone, which is not UTC but America/New_York. Setting both variables to the same value avoids drift between trigger times and log timestamps. The finer points (daylight saving time, cron expressions, per-workflow timezones) are covered in our Schedule Trigger and timezones guide.

Executions and pruning

Execution history is the number one cause of a bloated database on an instance that's been running for months:

Variable Default Role
EXECUTIONS_DATA_PRUNE true Enables automatic pruning of old executions
EXECUTIONS_DATA_MAX_AGE 336 (hours, i.e. 14 days) Maximum age of retained executions
EXECUTIONS_TIMEOUT -1 (disabled) Maximum duration of an execution in seconds, after which it is stopped

Pruning is enabled by default on recent versions, but checking these two values on an instance inherited from an old installation is a healthy reflex. EXECUTIONS_TIMEOUT deserves an explicit value: without it, a workflow stuck on an external call that never answers can stay "running" indefinitely and hold resources for nothing.

Binary data

Two variables already covered in depth in our large files and binary data guide, summarized here:

Variable Default Role
N8N_DEFAULT_BINARY_DATA_MODE default (memory) How files are stored during execution: default, filesystem, or s3 (Enterprise)
N8N_PAYLOAD_SIZE_MAX 16 (MB) Maximum size of an accepted JSON payload

Switching to filesystem is the first setting to flip once a workflow routinely handles files of several dozen MB — before even adding RAM to the server.

Security

Variable Role
N8N_ENCRYPTION_KEY Encryption key for every credential stored in the database
N8N_BLOCK_ENV_ACCESS_IN_NODE If true, blocks $env access from workflows

N8N_ENCRYPTION_KEY is the most critical variable in this entire list. If you don't set it explicitly, n8n generates one at first startup and stores it in its data folder — and the day you restore a database backup onto a new server without that key, every credential becomes undecryptable: every API connection, every OAuth, every password has to be re-entered by hand. Set it explicitly at installation time, store it in a secrets manager, and never change it on a production instance. It's the essential companion to the practices described in our guide to securing API credentials in n8n.

N8N_BLOCK_ENV_ACCESS_IN_NODE=true is the matching safeguard: on an instance shared between teams or exposed to workflows imported from external sources, it prevents a Code node or an expression from reading $env.DB_POSTGRESDB_PASSWORD or the encryption key itself.

Queue mode

To scale beyond a single process, n8n offers a queue mode built on Redis:

Variable Role
EXECUTIONS_MODE queue to enable queue mode (default: regular)
QUEUE_BULL_REDIS_HOST Host of the Redis server used as the queue

The main process receives triggers and pushes executions into Redis; separate workers consume them. Architecture, sizing, and pitfalls (including shared binary storage) are detailed in our queue mode with Redis guide.

Accessing variables from a workflow: $env

Every environment variable of the n8n process can be read in expressions via $env:

{{ $env.API_BASE_URL }}

It's the ideal mechanism for anything that changes between environments without being a secret in the strict sense: an API base URL, a bucket name, the ID of a Slack notification channel. The same $env object is available in a Code node — see our expressions and Code node guide for usage patterns. Reminder: if N8N_BLOCK_ENV_ACCESS_IN_NODE=true is set, this access is blocked everywhere.

$env is not $vars

Don't confuse it with n8n's "Variables" feature in the UI, accessed via $vars in expressions: those are created and edited from the interface without touching the server, but they belong to n8n's paid feature set (they're not part of the free Community Edition). $env reads the process environment — free, self-hosted only, restart required on every change; $vars reads values managed in the UI — editable on the fly, but gated by your plan. On a Community Edition, $env remains the standard route.

Best practices: treat your .env like code

Three rules prevent most incidents:

  1. No hardcoded secrets in workflows. An API key pasted into an HTTP Request node ends up in every JSON export of the workflow, every screenshot, every share. Secrets belong in n8n credentials (encrypted by N8N_ENCRYPTION_KEY); non-secret configuration belongs in $env.
  2. Document the .env. One comment per variable: what it's for, who set it, what breaks if it changes. A committed .env.example (with dummy values) serves as the contract for rebuilding the instance.
  3. Restart after every change, and log it. A variable changed without a restart gives you an instance whose displayed configuration doesn't match its actual behavior — the worst possible debugging scenario.

This isn't overkill. In their NeurIPS 2015 paper, Hidden Technical Debt in Machine Learning Systems (see it on Google Scholar), Sculley and his co-authors at Google identify configuration as one of the main sources of technical debt in production systems — to the point that the number of lines of configuration can exceed the number of lines of code, while being far less tested or reviewed. The finding applies word for word to an n8n instance: an undocumented .env is an outage waiting to happen. For teams with traceability requirements, the Compliance & Audit Pack ($149) takes this logic all the way, with inventory and audit-trail workflows that document the state of the instance continuously.

Going further

Environment variables are the foundational configuration layer of a self-hosted n8n: once N8N_ENCRYPTION_KEY is backed up, WEBHOOK_URL is aligned with the reverse proxy, the timezone is set, and execution pruning is verified, the instance is built to last. The next step is structuring the rest of the lifecycle — notably separating a test instance from a production one, each with its own .env, a topic covered in our operations guides. A clean .env is also what makes updates and migrations uneventful: it's what guarantees that an instance rebuilt from scratch behaves exactly like the old one.

FAQ

Frequently asked questions

How do you set an environment variable in self-hosted n8n?

With Docker, two approaches: add the variable to the environment block of the n8n service in docker-compose.yml, or put it in a .env file next to docker-compose.yml and reference it with the ${MY_VARIABLE} syntax. Either way, a container restart (docker compose up -d) is required for the change to take effect — n8n only reads its environment variables at startup.

Can you use environment variables on n8n Cloud?

No. On n8n Cloud, the infrastructure is managed by n8n and you have no access to the server configuration: you can't define your own environment variables or change the instance's. The Cloud-side alternatives are credentials for secrets and the Variables feature in the UI (accessed via $vars, available on paid plans that include it).

What is N8N_ENCRYPTION_KEY and why is it critical?

It's the key that encrypts every credential stored in n8n's database. If you restore a backup or migrate to a new server without keeping exactly the same key, every credential becomes undecryptable and has to be re-entered by hand. Set it explicitly, store it in a secrets manager, and never change it on a production instance.

How do you read an environment variable from an n8n workflow?

With $env in any expression, for example {{ $env.API_BASE_URL }}, or via $env in a Code node. This access can be disabled by setting N8N_BLOCK_ENV_ACCESS_IN_NODE=true, a useful safeguard on a shared instance to prevent a workflow from reading configuration secrets such as database passwords.

Bundle FlowKit Complet

€269