# 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.

URL: https://crunu.com/environment-variables/nodejs
Language: en
Updated: 2026-10-30

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](https://nodejs.org/api/cli.html) 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.

*Figure: Diagram of how Node.js loads environment variables. No env files are read by default; values come from the environment of the process. Values are read from the environment when the app starts. There is no public prefix: nothing reaches the browser unless your code sends it.*

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](https://crunu.com/integrations/systemd) shows the unit file, and [secrets on servers without files on disk](https://crunu.com/blog/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](https://crunu.com/guides/share-env-files-with-a-team) compares the options, and [keeping .env.example in sync](https://crunu.com/blog/keep-env-example-in-sync) covers the template itself.

## Related reading

- [Bun](https://crunu.com/environment-variables/bun) loads `.env` files automatically, the opposite default.
- [Docker Compose](https://crunu.com/environment-variables/docker-compose) for Node.js in containers.
- [Why dotenv parsers disagree](https://crunu.com/blog/dotenv-parsers-disagree) on quotes, comments and expansion.
- [crunu run](https://crunu.com/docs/cli/run) and [installing the agent on a server](https://crunu.com/docs/getting-started/install-the-agent).

## FAQ

### 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

- [Node.js command-line options: --env-file](https://nodejs.org/api/cli.html) (accessed 2026-10-30)
- [Node.js: Environment variables](https://nodejs.org/api/environment_variables.html) (accessed 2026-10-30)
