CLI Options, Environment & Config File
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with
the differences of Node.js 24 "Krypton" (24.21.0, Active LTS) and Node.js 22 "Jod" (22.23.3, Maintenance) called out; all
versions and stability levels were checked on 2026-10-03 and every example below was run on all three lines.
Almost everything that changes how a Node.js process behaves can be decided before a single line of your code runs: by a
command line flag, by an environment variable, by a .env file or by a JSON configuration file. These are four layers of one
mechanism, and knowing which layer wins, and which flags are even allowed in which layer, removes most "works on my machine"
surprises in the Bookshelf application that grows across this section. The APIs covered here carry these stability
levels: --env-file, --env-file-if-exists, process.loadEnvFile() and util.parseEnv() are Stable (the dotenv support as
a whole is Stability 2, no longer experimental since v24.10.0 and v22.21.0); --require carries no stability marker in the CLI documentation; --disable-warning is
Stable since v26.7.0 (and v24.20.0); --import is documented as Stability 1 Experimental on every line even though it is
widely used; and the configuration file --experimental-config-file is Stability 1.2 Release candidate since v26.7.0 (it is
still 1.0 Early development in the Node.js 24 and 22 documentation).
Where options come from
A node process is configured from several places, and the order in which they are applied is the part worth memorising.
| Layer | Example | Rule |
|---|---|---|
Configuration file |
|
Loaded with |
|
|
Interpreted before the command line. Command line flags override or compound after it. |
Command line |
|
Singleton flags (such as |
Environment of the process |
|
Values already present in the real environment win over a |
The documentation describes the pairs separately, so the order of the configuration file was also measured: with a
nodeOptions entry for the singleton flag --dns-result-order, the value from the file applied alone, NODE_OPTIONS
overrode it, and the command line overrode both, on Node.js 26, 24 and 22. The configuration file: node.config.json shows the file in use.
The node binary itself reads options in this form: node [options] [script.js] [arguments]. Everything after the script
name belongs to your program and shows up in process.argv; everything before it belongs to Node.js. A lone -- ends the
option list.
Flags by category
node --help lists about two hundred options on Node.js 26, and the full reference is the CLI page of the API
documentation. Nobody memorises that list; what helps is a mental map. The table groups the flags you meet most often. Names
are exactly as they appear in node --help on Node.js 26.10.0.
| Category | Flags | Purpose |
|---|---|---|
Run and preload |
|
Inline code, syntax check, preload modules, run a |
Environment and config |
|
Load |
Modules and TypeScript |
|
Conditional exports, how |
Warnings and deprecations |
|
Silence, escalate or trace process warnings. See |
Diagnostics |
|
Debugger, profilers, diagnostic reports, heap snapshots near the limit. |
Memory and V8 |
|
Heap limit and stack trace length are V8 options that Node.js passes through. See |
Security and permissions |
|
Permission model (Stable, except |
Networking |
|
DNS ordering, HTTP header limit, happy-eyeballs behaviour. |
Test runner |
|
See the Test Runner pages of this section. |
Two spelling rules cut across all categories. Boolean flags usually have a --no- form (--no-warnings,
--no-deprecation), and value flags use =, as in --max-old-space-size=512. Only some flags accept a space between the
flag and its value, and the configuration file section below shows a flag whose space form is explicitly not supported.
NODE_OPTIONS
NODE_OPTIONS is a space-separated list of command line options that Node.js applies before the real command line.
That makes it the right tool for settings that must apply to every Node.js process started by a tool you do not control: a
test runner spawning workers, a process manager, a container entry point, a CI job.
node heap.js
NODE_OPTIONS=--max-old-space-size=256 node heap.js
NODE_OPTIONS='--import ./preload-esm.mjs' node -r ./preload-cjs.cjs main.mjs
NODE_OPTIONS='-p 1' node main.mjs
The heap.js script prints getHeapStatistics().heap_size_limit from node:v8. On Node.js 26.10.0 on a machine with
plenty of memory the output was:
heap limit: 4192 MiB
heap limit: 352 MiB
1: --require preload (CommonJS)
2: --import preload (ESM)
3: main module
node: -p is not allowed in NODE_OPTIONS
The absolute numbers depend on the machine and on the Node.js line (they were 4288 and 448 MiB on Node.js 24.21.0, and
4144 and 304 MiB on 22.23.3), and the limit reported is larger than the old-space value you passed because it also
includes the other V8 heap spaces. The two lessons are independent of the numbers. First, not every flag may be placed in
NODE_OPTIONS: Node.js exits with an error for a flag that is not allowed, such as -p or a script file. The allowed list
is printed in the CLI documentation and, at run time, in process.allowedNodeEnvironmentFlags, a set of 314 entries on
Node.js 26.10.0. Second, when a repeatable flag appears in both places the NODE_OPTIONS instances run first.
import process from 'node:process';
const allowed = process.allowedNodeEnvironmentFlags;
console.log(allowed.has('--import'), allowed.has('--eval'));
console.log(allowed.size > 0);
The script printed true false and then true on all three lines: --import is accepted in NODE_OPTIONS, --eval is not.
A value that contains a space is escaped with double quotes inside the variable, for example
NODE_OPTIONS='--require "./my path/file.js"'. The same variable can come from a .env file loaded with --env-file;
the next example sets a heap limit that way and prints the same number as the shell form.
printf 'NODE_OPTIONS=--max-old-space-size=256\n' > .env.node
node --env-file=.env.node heap.js
The result was heap limit: 352 MiB on Node.js 26.10.0, identical to the shell form. Note the asymmetry that the
documentation points out: NODE_OPTIONS written in a .env file is applied by --env-file, but it has no effect when
the file is read later by process.loadEnvFile(), because by then the process has already started.
NODE_DEBUG
NODE_DEBUG is a comma-separated list of core module names that should print debug information to standard error. The
names are the internal tags of the modules (fs, http, module, net, timers and others); an unknown tag simply prints
nothing. Your own code can join the same mechanism with util.debuglog(), which produces output only when its section
name is in NODE_DEBUG, and the matching is case-insensitive.
import { readFile } from 'node:fs/promises';
import { debuglog } from 'node:util';
const log = debuglog('bookshelf');
log('loading catalogue %s', 'books.json');
await readFile(new URL(import.meta.url)); // exercise the fs module
Source: Node.js API: util.debuglog()
Run it with NODE_DEBUG=bookshelf,module node debug.js. On all three lines the output started with these lines, where the
number is the process id (so it differs on each run):
MODULE 66773: load built-in module fs/promises
MODULE 66773: load built-in module util
BOOKSHELF 66773: loading catalogue books.json
NODE_DEBUG_NATIVE is the same switch for core C++ modules. Both are developer tools: they write to standard error, can
include paths and request details, and belong in a local shell, not in a production environment file.
NODE_PATH (CommonJS only)
NODE_PATH is a list of directories that is added to the module search path used by require(). The separator is the
operating system path separator: : on Linux and macOS, ; on Windows. It is a legacy mechanism for sharing
modules across projects, and it exists only in the CommonJS resolver. ECMAScript modules ignore it, which is one of the
documented differences between the two systems.
# vendor/shelf-utils/package.json declares "type": "commonjs"; the package is not in node_modules
NODE_PATH=./vendor node use-cjs.cjs
NODE_PATH=./vendor node use-esm.mjs
node use-cjs.cjs
efficient-node
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'shelf-utils' imported from .../use-esm.mjs
Error: Cannot find module 'shelf-utils'
The first run succeeds, the ESM import fails with ERR_MODULE_NOT_FOUND even with the variable set, and without the variable
the CommonJS require() fails with MODULE_NOT_FOUND. The output is identical on 26, 24 and 22. The practical advice is
to treat NODE_PATH as a fallback for old tooling: dependencies belong in node_modules, shared code in workspaces
(see JavaScript Modules), and absolute-looking imports in the
imports field of package.json. The module-resolution pages of this section cover both options.
Preloading: -r versus --import
Both flags load a module before the main entry point, in the main thread and also in worker threads, forked processes and clustered processes. They differ in which module system resolves the specifier and when they run.
-r, --require module |
--import module |
|
|---|---|---|
Resolution |
Follows |
Follows ECMAScript module resolution rules |
Runs |
Before every |
After every |
Typical use |
Instrumentation agents and loaders written for CommonJS |
Registering hooks, loading polyfills, telemetry initialisation written as ESM |
Stability |
No marker in the CLI documentation |
Stability 1 Experimental (documented), widely used |
node -r ./preload-cjs.cjs --import ./preload-esm.mjs main.mjs
node --import ./preload-esm.mjs -r ./preload-cjs.cjs main.mjs
Both commands printed the same three lines on Node.js 26, 24 and 22, so the documented rule holds: all --require
modules run first, then all --import modules, then the main module.
1: --require preload (CommonJS)
2: --import preload (ESM)
3: main module
The pattern most people know, node -r dotenv/config index.js, is exactly this mechanism. Since v23.0.0 (and backported to
v22.12.0 and v20.19.0), --require can also load an ECMAScript module, which is why passing an ESM file to -r works on
all three lines.
node -r ./preload-esm.mjs main.mjs
The output was 2: --import preload (ESM) followed by 3: main module on 26, 24 and 22. Preloading is also available per
project: both flags can be written in NODE_OPTIONS or in the nodeOptions section of the configuration file below. For
one more, related preload mechanism see module.register() and module.registerHooks() on the modules pages of this
section; note that module.register() has been a runtime deprecation (DEP0205) since v26.0.0.
Environment files
An environment file (a .env file) is a plain text file of KEY=value lines. Node.js has had a built-in loader since
v20.6.0 and it has no external dependency. There is no formal .env specification, so Node.js defines its own, described on
the Environment Variables page of the API documentation.
# Bookshelf local settings (placeholders only)
PORT=3000
DATABASE_URL="postgres://bookshelf:change-me@localhost:5432/bookshelf" # local dev only
API_KEY=replace-with-a-generated-key
GREETING="hello
second line"
export LOG_LEVEL=debug
The grammar, as the documentation defines it:
| Element | Rule |
|---|---|
Names |
Letters, digits and underscores, not starting with a digit (the regular expression |
Values |
Any text, optionally wrapped in single or double quotes (the documentation also lists the backtick); quoted values can span several lines, unquoted ones cannot |
Types |
Every value is a string: |
Spacing |
Whitespace around keys and values is ignored unless it is inside quotes |
Comments |
A |
|
Ignored, so the same file can be sourced by a shell |
--env-file and --env-file-if-exists
--env-file=file loads a file relative to the current directory into process.env before your program starts. It can be
repeated, and later files override variables defined by earlier ones. A variable that is already present in the real
environment wins over the file, so a container or CI job can always override the file without editing it.
--env-file-if-exists behaves the same but does not throw when the file is absent, which suits optional per-developer
overrides.
node --env-file=.env show-env.js
node --env-file=.env --env-file=.env.local show-env.js
PORT=7000 node --env-file=.env --env-file=.env.local show-env.js
node --env-file=.env.nope show-env.js
node --env-file-if-exists=.env.nope show-env.js
With .env.local containing only PORT=4000, and show-env.js printing four variables, the runs produced (Node.js 26.10.0;
24.21.0 and 22.23.3 print the same values):
{ PORT: '3000', DATABASE_URL: 'postgres://bookshelf:change-me@localhost:5432/bookshelf', LOG_LEVEL: 'debug', GREETING: 'hello\nsecond line' }
{ PORT: '4000', ... } # the second file overrides the first
{ PORT: '7000', ... } # the shell wins over both files
node: .env.nope: not found # --env-file: a missing file is an error
.env.nope not found. Continuing without it. # --env-file-if-exists: a notice, then PORT is undefined
The first line is abbreviated onto one line here; Node.js prints the object across several lines. The quoted multi-line
value became a single string containing a newline, the comment and the export prefix were ignored, and the quotes were
removed. Two details worth knowing: variables that configure Node.js itself, such as NODE_OPTIONS, are parsed and
applied from the file (see the earlier example), and the file path is resolved against the current working directory, not
against the script. Multi-line value support arrived in v20.12.0 and v21.7.0.
// show-env.js
import process from 'node:process';
const { PORT, DATABASE_URL, LOG_LEVEL, GREETING } = process.env;
console.log({ PORT, DATABASE_URL, LOG_LEVEL, GREETING });
process.loadEnvFile() and util.parseEnv()
The two programmatic APIs cover the cases where the file is chosen at run time. process.loadEnvFile(path) loads the file
into process.env; the path defaults to ./.env and may be a string, a URL or a Buffer. util.parseEnv(content) only
parses the text and returns an object, leaving process.env untouched, which is what you want for tests, for values that
should not leak into child processes, and for files fetched from a secret store.
import { loadEnvFile } from 'node:process';
import { parseEnv } from 'node:util';
import { readFileSync } from 'node:fs';
process.env.PORT = '9999'; // already set: loadEnvFile() does not override
loadEnvFile('.env');
console.log('PORT after loadEnvFile:', process.env.PORT);
console.log('LOG_LEVEL:', process.env.LOG_LEVEL);
const parsed = parseEnv(readFileSync('.env.local', 'utf8'));
console.log('parseEnv:', parsed);
console.log('process.env.PORT still:', process.env.PORT);
PORT after loadEnvFile: 9999
LOG_LEVEL: debug
parseEnv: { PORT: '4000' }
process.env.PORT still: 9999
On Node.js 26.10.0 the object printed by parseEnv is shown as [Object: null prototype] { PORT: '4000' }, while 24 and 22
print { PORT: '4000' }. Treat the result as a dictionary, not as an ordinary object: do not call hasOwnProperty on it
and use Object.hasOwn() or the in operator instead. The documentation example for parseEnv shows that the last
definition of a repeated key wins (HELLO=world followed by HELLO=oh my gives oh my).
Both APIs are Stable since v24.10.0 and v22.21.0 (added in v21.7.0 and v20.12.0). Before that they were experimental, which is why many Node.js 20 articles still reach for a package.
Why the dotenv package is now optional
The dotenv package popularised the format, and node -r dotenv/config or dotenv.config() still work. For the common
case, though, the built-in loader removes a dependency: no install, no supply-chain surface, no second parser to keep
in sync with the platform, and the same behaviour on every host that has Node.js 22 or later. The package remains useful when
you need features the built-in format deliberately lacks, for example variable expansion between entries, or when you must
support Node.js releases older than 20.6. Frameworks have their own layering, as in
NestJS configuration, which builds on dotenv.
A few safe habits apply to every approach. Commit a .env.example containing placeholders and keep the real .env out of
version control. Generate secrets rather than inventing them, for example with
node -p "crypto.randomBytes(32).toString('hex')". Remember that every value in process.env is inherited by the child
processes you spawn, and that --trace-env (next section) will print access to environment variables. In production prefer
the secret mechanism of the platform and let the real environment override the file, which --env-file already does.
PORT=1 node --trace-env -e "process.env.PORT"
The flag was added in v23.4.0 and v22.13.0 and prints one [--trace-env] get "NAME" line per read, including reads that
Node.js performs internally, so the output shows both "PORT" and variables such as "WATCH_REPORT_DEPENDENCIES".
--trace-env-js-stack and --trace-env-native-stack add stack traces to each line.
The configuration file: node.config.json
Instead of a long list of flags, a project can keep its Node.js settings in a JSON file. The flag is
--experimental-config-file=path; without a path it reads node.config.json from the current working directory, and
--experimental-default-config-file is an alias for the no-path form. The space-separated form
--experimental-config-file path is not supported. The feature was added in v23.10.0 and v22.16.0; it is Stability 1.2
Release candidate since v26.7.0. Node.js 26.10.0 still prints an ExperimentalWarning for it, as do 24 and 22.
{
"$schema": "https://nodejs.org/dist/latest-v26.x/docs/node-config-schema.json",
"nodeOptions": {
"import": ["./preload-esm.mjs"],
"disable-warning": ["BOOKSHELF001"],
"watch-preserve-output": true
}
}
The file has these parts:
| Key | Meaning |
|---|---|
|
The official JSON schema URL, so editors can validate the file. Replace |
|
CLI flags that are allowed in |
Namespaces ( |
Settings for a subsystem. When a namespace is present Node.js turns on the matching flag ( |
|
An integer major version. The file applies only when it matches the running major version. |
|
An array of entries, each with its own |
Unknown or not-allowed keys are rejected, and V8 flags are only partly supported.
node --experimental-config-file=node.config.json warn.js
node --experimental-default-config-file main.mjs
echo '{"nodeOptions":{"no-such-flag":true}}' > bad.json
node --experimental-config-file=bad.json main.mjs
node --experimental-config-file=node.test.config.json t/a.test.js
With node.test.config.json containing only { "test": { "test-isolation": "process" } }, the observed behaviour on 26.10.0,
24.21.0 and 22.23.3 was:
2: --import preload (ESM) # from the file: the import ran, and DEP warning BOOKSHELF001 was silenced
Unknown or not allowed option no-such-flag for namespace nodeOptions
node: bad.json: invalid content
✔ ok (0.24ms) # the test namespace switched the test runner on
Each run also printed the single experimental warning about the config file. The second command needs no flag value because
the default file name is node.config.json. The file is the weakest layer: the same flag in NODE_OPTIONS or on the command line
replaces its value (see Where options come from). The format is young: the schema depends on the Node.js
version, so pin the $schema URL to the line you run.
--disable-warning and its relatives
Node.js writes process warnings (deprecations, experimental features, memory leak warnings) to standard error through
process.emitWarning(). Each warning has a type (DeprecationWarning and ExperimentalWarning are the core types) and may
have a code such as DEP0025. There are four levers, from coarse to fine:
| Option | Effect |
|---|---|
|
Silence every process warning. Simple, but hides things you want to see. |
|
Silence warnings that match a code or a type. Repeatable, and allowed in |
|
Silence or escalate every deprecation warning (the second turns them into exceptions, handy in CI). |
|
Print a stack trace for the code that raised the warning, to find out who is calling a deprecated API. |
// warn.js
import process from 'node:process';
process.emitWarning('legacy catalogue format', { type: 'DeprecationWarning', code: 'BOOKSHELF001' });
process.emitWarning('new search ranking', { type: 'ExperimentalWarning' });
console.log('done');
node warn.js
node --disable-warning=BOOKSHELF001 warn.js
node --disable-warning=DeprecationWarning --disable-warning=ExperimentalWarning warn.js
node --no-warnings warn.js
done
(node:58873) [BOOKSHELF001] DeprecationWarning: legacy catalogue format
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:58873) ExperimentalWarning: new search ranking
The first command prints both warnings. The second prints only the ExperimentalWarning, the third and fourth print nothing
but done. The behaviour is identical on all three lines. Prefer the narrowest switch that solves the problem: a code (such
as a DEP number from the deprecation list) when you have decided to live with one known warning, a type when a whole class
is noise, and --no-warnings only for throwaway scripts. Silencing deprecations permanently is how projects get
caught by a removal in the next major release.
--v8-options and V8 flags
Node.js embeds V8, and V8 has its own flags. node --v8-options prints them all, with a description, type and default for
each. On Node.js 26.10.0 the listing is about 1,900 lines long, so pipe it through a pager or grep.
node --v8-options | wc -l
node --v8-options | grep -A1 -E "^ --max-old-space-size"
node --max-old-space-size=512 --stack-trace-limit=50 app.js
Two V8 options are common in production: --max-old-space-size=SIZE (the heap limit in MiB, also shown above through
NODE_OPTIONS) and --stack-trace-limit=limit. Many V8 flags are not allowed in NODE_OPTIONS; the CLI page lists the
supported ones, and the v8 module (v8.setFlagsFromString()) can set a few at run time but is not a supported way to
change behaviour. In a container, align the heap limit with the memory limit of the container instead of leaving the
default, and read the deployment pages of this section for the details.
What changed recently
| Version | Change |
|---|---|
v20.6.0 / v20.12.0 |
|
v22.9.0 |
|
v22.12.0 / v23.0.0 |
|
v22.13.0 / v23.4.0 |
|
v22.16.0 / v23.10.0 |
|
v24.10.0 / v22.21.0 |
The dotenv support ( |
v24.20.0 / v26.7.0 |
|
v26.7.0 |
|
v26.0.0 |
|
Where the books are outdated
Efficient Node.js (Samer Buna, O’Reilly 2025) is largely current on this topic: it already mentions the built-in env file
loader, NODE_OPTIONS, NODE_DEBUG and NODE_PATH. Three points need updating or sharpening. It describes --require as
CommonJS-only and --import as the ESM counterpart, which was true until v22.12.0 and v23.0.0; today -r can load an ESM
file too, while the ordering rule (all -r modules before all --import modules) is unchanged. Its example of defining
NODE_DEBUG in an env file does work (checked on 26, 24 and 22), and since its publication the loader has become Stable (v24.10.0
and v22.21.0) and gained --env-file-if-exists and --trace-env. Finally, NODE_PATH is described
as a way to shorten import paths; it only affects CommonJS resolution, so for ESM and TypeScript use the imports field.
Node.js Projects (Jonathan Wexler, O’Reilly 2025) loads secrets with the dotenv package and dotenv.config() inside the
application code, with the sample key kept in a .env file. That pattern is still functional, but the built-in --env-file
flag and process.loadEnvFile() make the dependency unnecessary on Node.js 22 and later, and passing the file on the command
line keeps the loading code out of the program. Whatever approach you take, never paste a real key into a document or commit
it: generate secrets and keep them in untracked env files or the platform’s secret store.
References
-
Node.js API: Command-line options, https://nodejs.org/docs/latest/api/cli.html
-
Node.js API: Environment Variables, https://nodejs.org/docs/latest/api/environment_variables.html
-
Node.js API: Process (
process.loadEnvFile(),process.allowedNodeEnvironmentFlags,process.emitWarning()), https://nodejs.org/docs/latest/api/process.html -
Node.js API: Utilities (
util.parseEnv(),util.debuglog()), https://nodejs.org/docs/latest/api/util.html -
Node.js API: Modules: CommonJS modules, https://nodejs.org/docs/latest/api/modules.html
-
Node.js Learn: How to read environment variables from Node.js, https://nodejs.org/learn/command-line/how-to-read-environment-variables-from-nodejs
-
Node.js API: Deprecated APIs, https://nodejs.org/docs/latest/api/deprecations.html
-
O’Reilly: Efficient Node.js, Samer Buna, https://www.oreilly.com/library/view/efficient-node-js/9781098145187
-
O’Reilly: Node.js Projects, Jonathan Wexler, https://www.oreilly.com/library/view/node-js-projects/9781098173135/