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.
Part of the guide
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
- Crunu command
crunu run --env api/development -- node --watch src/server.jsSee the systemd integration
Common pitfalls
- Node.js reads no .env file unless you ask: without --env-file or loadEnvFile, process.env holds only what the parent process passed.
- --env-file throws when the file is missing. Use --env-file-if-exists for a file that only some machines have.
- A variable already in the environment beats the same name in the env file, so a stale value exported in your shell wins.
- 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.
- 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:
- Variables already in the environment when Node.js starts.
- The last
--env-fileon the command line. - Earlier
--env-fileflags, 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.localfails 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 optionalexportprefix that is ignored.
In code: loadEnvFile and parseEnv
Two functions cover the cases where a flag is inconvenient:
process.loadEnvFile(path)loads a file intoprocess.envat that point in your code.util.parseEnv(text)parses the contents of an env file and returns an object without touchingprocess.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.
- Sign in on your machine with
crunu login, then develop withcrunu run --env api/development -- node --watch src/server.js. - Run tests against their own environment, for example
crunu run --env api/test -- npm test, so test credentials never mix with development ones. - On a Linux server, install the agent, grant the machine
api/production, and start the service withcrunu 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,SHELLandTMPDIRfrom your shell, plus the Crunu variables.NODE_ENVandNODE_OPTIONSfrom your shell don't pass through. Put them in the Crunu environment, or use--inherit-envwhen you need the whole shell. - The token stays out of reach. In CI,
crunu runnever passesCRUNU_TOKENto 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.
Related reading
- Bun loads
.envfiles automatically, the opposite default. - Docker Compose for Node.js in containers.
- Why dotenv parsers disagree on quotes, comments and expansion.
- crunu run and installing the agent on a server.
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.
- Node.js command-line options: --env-file · checked October 30, 2026
- Node.js: Environment variables · checked October 30, 2026