Skip to content
RuntimeJavaScript

Node.js environment variables: --env-file and dotenv

Node.js reads only process.env unless you pass --env-file. How the flag, loadEnvFile and dotenv behave, which value wins, and how crunu run fits in.

Node.jsAt a glance

Env files, highest priority first
No .env files: values come from the process environment or the platform.
Public prefix
None

No names are copied into browser code by a prefix.

Values applied
At runtime

Values are read from the environment when the app runs.

Built-in secret store
None: process.env is inherited from the parent process

nodejs.org/api/cli.html

Crunu command
crunu run --env api/development -- node --watch src/server.jsSee the systemd integration

Common pitfalls

  1. Node.js reads no .env file unless you ask: without --env-file or loadEnvFile, process.env holds only what the parent process passed.
  2. --env-file throws when the file is missing. Use --env-file-if-exists for a file that only some machines have.
  3. A variable already in the environment beats the same name in the env file, so a stale value exported in your shell wins.
  4. NODE_OPTIONS inside an env file is parsed and applied, so the file can change how Node.js starts, not only what the app reads.
  5. Every value is a string. PORT=0 and DEBUG=false arrive as non-empty strings until you convert them.
On this page

A Node.js program reads its environment variables from process.env, which is a copy of the environment of the process that started it. Node.js doesn't look for a .env file on its own. It loads one only when you pass --env-file=FILE on the command line or call process.loadEnvFile() in code, and even then a variable that already exists in the environment keeps its value. That makes Node.js the simplest stack on this list: whatever starts the process decides what the program sees. The flag behavior below is from the Node.js command-line documentation for version 26, checked October 30, 2026.

Where process.env comes from

When you type node server.js in a terminal, Node.js inherits the variables of your shell. Under systemd it gets the unit's Environment= and EnvironmentFile= values. In a container it gets what the image and the runtime set. Node.js adds nothing of its own and reads no files unless told to.

The diagram above has no file rows because Node.js has no default file list. With --env-file, the order is simple:

  1. Variables already in the environment when Node.js starts.
  2. The last --env-file on the command line.
  3. Earlier --env-file flags, each overridden by the ones after it.

So node --env-file=.env --env-file=.env.development app.js lets .env.development override .env, and an exported shell variable overrides both.

The --env-file flag in practice

The flag arrived in Node.js 20.6.0, gained multi-line values in 21.7.0 and 20.12.0, and stopped being experimental in 24.10.0 and 22.21.0. A few details are easy to miss:

  • A missing file is an error. --env-file=.env.local fails if a teammate never created that file. --env-file-if-exists, added in 22.9.0, behaves the same but skips missing files.
  • Paths are relative to the current directory, not to the script. Running the same command from another folder reads a different file or none.
  • Node.js settings in the file are applied. If the file contains NODE_OPTIONS, Node.js parses it as though you had set it in the environment. A file meant for application settings can therefore change memory limits, preload modules or enable inspectors.
  • The file format is Node's own. There is no formal dotenv standard, so Node.js documents its rules: names made of letters, digits and underscores that don't start with a digit, # comments outside quotes, quoted values that may span lines, and an optional export prefix that is ignored.

In code: loadEnvFile and parseEnv

Two functions cover the cases where a flag is inconvenient:

  • process.loadEnvFile(path) loads a file into process.env at that point in your code.
  • util.parseEnv(text) parses the contents of an env file and returns an object without touching process.env.

Both use the same parser as the flag. Calling loadEnvFile late in startup is a common source of bugs: modules imported earlier have already read their configuration, and they won't see the new values.

Values are strings

Every value in process.env is a string. PORT=3000 gives "3000", ENABLE_CACHE=false gives "false", and if (process.env.ENABLE_CACHE) is true for both "true" and "false". Convert and validate at startup in one place, and fail fast when a required variable is missing, instead of discovering it on the first request that needs it.

The same parse-once rule also helps with restarts: since process.env is copied at start, changing a value in the environment of a running server does nothing until the process restarts.

Child processes inherit everything

When your program starts another program with child_process.spawn(), exec() or fork() and doesn't pass an env option, the child receives a copy of the whole process.env. A database password loaded for your API therefore also reaches the image converter, the PDF renderer or the shell script your code calls. Most of these tools never need it, but any of them that logs its environment, crashes with a dump, or is compromised can expose it.

The fix is cheap: pass an explicit env object with only the variables the child needs, and keep PATH in it so the program can still be found. The same thinking applies one level up, which is why crunu run starts your program with an allowlist rather than your entire shell, as described below.

Run Node.js with values from Crunu

crunu run starts your program with the variables of a Crunu environment in its process environment. To Node.js this looks like a variable set by the parent process, the highest-priority source, so no --env-file flag and no dotenv import are needed. The values are decrypted in memory and never written to disk.

  1. Sign in on your machine with crunu login, then develop with crunu run --env api/development -- node --watch src/server.js.
  2. Run tests against their own environment, for example crunu run --env api/test -- npm test, so test credentials never mix with development ones.
  3. On a Linux server, install the agent, grant the machine api/production, and start the service with crunu run --env api/production --restart on-change -- node dist/server.js.

With --restart on-change, crunu sends SIGTERM to Node.js after a value changes and starts it again with the new variables, waiting up to 30 seconds by default for a graceful shutdown. Handle SIGTERM in your server so in-flight requests can finish. The systemd integration shows the unit file, and secrets on servers without files on disk explains why this beats an EnvironmentFile= on a production server.

Two Crunu details matter for Node.js projects:

  • A clean environment. By default the program gets only PATH, HOME, USER, LOGNAME, LANG, LC_ALL, TERM, TZ, SHELL and TMPDIR from your shell, plus the Crunu variables. NODE_ENV and NODE_OPTIONS from your shell don't pass through. Put them in the Crunu environment, or use --inherit-env when you need the whole shell.
  • The token stays out of reach. In CI, crunu run never passes CRUNU_TOKEN to Node.js or to anything Node.js starts, such as a postinstall script.

What to keep in Git

Keep a .env.example with every variable name and a harmless placeholder, so new teammates and reviewers can see what the program expects. Keep real values out of the repository and out of chat. How to share .env files with your team compares the options, and keeping .env.example in sync covers the template itself.

Frequently asked questions

Something else? Write to support@crunu.com

Do I still need the dotenv package in Node.js?

Not for plain loading. Since Node.js 20.6 the --env-file flag loads a file into process.env, and it's no longer experimental since 24.10 and 22.21. Keep dotenv if you rely on features the built-in loader lacks or on very old Node.js versions.

I pass two --env-file flags. Which file wins?

The later one. Each file overrides variables defined by the files before it. But a variable that was already in the environment before Node.js started beats both files.

Do I need --env-file when I start Node.js with crunu run?

No. crunu run puts the variables straight into the process environment, so process.env already has them. You can leave --env-file out, or keep a non-secret defaults file with it, since the values from Crunu win.

Can a running Node.js process pick up a changed variable?

Not by itself. process.env is copied when the process starts. Restart the process, which crunu run does for you with --restart on-change.

Sources

Every source this page relies on, with the date we last checked it.

  1. Node.js command-line options: --env-file · checked October 30, 2026
  2. Node.js: Environment variables · checked October 30, 2026

Keep your .env values in one place

Free during early access. Values are encrypted on your devices, each environment has its own key, and crunu run hands them to your app without writing a file.