--env-file arrived in Node 20.6.0 and lost its experimental label in 24.10.0 and 22.21.0, so on every supported release line — 26.9.0 Current, 24.21.0 Active LTS, 22.23.2 Maintenance LTS — a process reads a .env file with no dependency at all. The parser is deliberately small: one KEY=value per line, # starts a comment, single, double or backtick quotes are stripped, a leading export is ignored, and a double-quoted value may span lines.
# service defaults, safe to commit
PORT=3000
LOG_LEVEL=info
export SERVICE_NAME="orders-api" # the export keyword is ignored
GREETING='hello # not a comment'const e = process.env;
console.log(e.PORT, e.LOG_LEVEL, e.SERVICE_NAME, JSON.stringify(e.GREETING));$ node --env-file=.env show.js 3000 info orders-api "hello # not a comment" $ node --env-file=.env --env-file=.env.development show.js 4000 debug orders-api "hello # not a comment" $ node --env-file=.env.production show.js node: .env.production: not found $ node --env-file=.env --env-file-if-exists=.env.production show.js .env.production not found. Continuing without it. 3000 info orders-api "hello # not a comment"
Pass the flag more than once and later files win, which is the layering a per-environment override needs. The real environment beats every file, so a value injected by your platform is never clobbered by a stale .env baked into the image. A missing file is a hard error, which is right for .env.production on a server and wrong for a teammate's .env.local; hence --env-file-if-exists.
The parser stops short of what dotenv 20,543 users expect: no interpolation, no command substitution, no encryption.