NodeJSEnvironment Variables (process.env)

Environment Variables (process.env)

Environment variables are key/value settings the operating system hands to your process when it starts. They are the standard way to configure a Node app without hard-coding values — database URLs, API keys, ports, feature flags, and the run mode all come from here. Node copies them onto process.env, where they become the single most common source of configuration in real deployments.

Where they come from

A child process inherits a snapshot of its parent's environment. Your shell sets variables; when it launches node, Node receives a copy. This chain — OS → shell/orchestrator → Node → any child processes you spawn — is why a variable set in one terminal is invisible in another:

The inheritance chain

Text
OS / login session
   └─ shell (or Docker / Kubernetes / systemd / CI runner)
        └─ node app.js          ← gets a COPY of the parent's env
             └─ child_process    ← gets a COPY of Node's env (tweak via {env})
Reading variables

JS
console.log(process.env.NODE_ENV)   // 'development' | 'production' | undefined
const port = process.env.PORT || 3000
const dbUrl = process.env.DATABASE_URL
Everything is a string — coerce deliberately
Values on `process.env` are **always strings** (or `undefined`). This causes classic bugs: `Number(process.env.PORT)` for numbers; and for booleans, never write `if (process.env.DEBUG)` — the string `'false'` is **truthy**! Compare explicitly: `process.env.DEBUG === 'true'`.

JS
Boolean('false')                       // true  ← the trap
process.env.FEATURE_X === 'true'       // correct boolean check
Number(process.env.MAX || '100')       // correct numeric parse with default
Reading vs writing
Writing mutates only your own process
You *can* assign — `process.env.FOO = 'bar'` — and Node coerces the value to a string. But it only changes **this process and its future children**; it does not touch the parent shell or any sibling process. There is no way to "export back" to the shell that launched you. Treat `process.env` as read-mostly configuration.
Setting variables when you run

Shell

Command

macOS / Linux (bash/zsh)

PORT=8080 NODE_ENV=production node app.js

Windows PowerShell

$env:PORT=8080; $env:NODE_ENV="production"; node app.js

Windows cmd.exe

set PORT=8080 && node app.js

Cross-platform (npm script)

cross-env PORT=8080 node app.js

The inline form is one-shot
On macOS/Linux, `PORT=8080 node app.js` sets `PORT` **only for that one command**. The variable does not persist in your shell afterwards — which is exactly what you want for a quick run.
The .env file pattern

Listing variables on the command line gets unwieldy fast. The convention is a .env file in the project root. Modern Node loads it natively with --env-file; older versions use the dotenv package.

.env

Bash
PORT=8080
NODE_ENV=development
DATABASE_URL=postgres://localhost:5432/app
JWT_SECRET=super-secret-value

Native (Node 20.6+) — no dependency needed

Bash
node --env-file=.env app.js

Or with dotenv (older Node)

JS
import 'dotenv/config'   // loads .env into process.env at startup
Real env wins over the file
Both `--env-file` and `dotenv` only set a key if it is **not already** in the environment. So a value injected by your host (Docker, Kubernetes, the cloud dashboard) takes precedence over the file — letting the same code run locally from `.env` and in production from real env vars.
NODE_ENV and run modes

By convention NODE_ENV is 'production' in prod and 'development' locally. Many libraries (Express, React) optimize or add safety checks based on it — Express caches view templates and skips verbose errors when it is 'production':

JS
const isProd = process.env.NODE_ENV === 'production'
app.use(isProd ? compression() : devLogger())
NODE_ENV is not a security boundary
It is just a string anyone can set. Do not use it to gate access to secrets or admin features — a typo or a misconfigured deploy silently flips behaviour. Use it only for performance/diagnostics toggles.
Security: never commit secrets
Keep .env out of git
A `.env` file holds secrets (API keys, DB passwords). **Add it to `.gitignore`** and never commit it. Commit a `.env.example` with empty/placeholder values to document what is required. In production, inject variables via your host/orchestrator — not a file baked into an image.
  • Validate required vars at startup and fail fast if any are missing.

  • Never log full secret values — mask them (sk_live_****) if you must log presence.

  • Use a secrets manager (Vault, AWS/GCP Secrets Manager) for sensitive production data.

  • Scope secrets to the smallest surface — a leaked .env in a build log is a real incident. See Dependency & Secret Security.

Validating at startup

Fail loudly the moment the process boots, not deep inside a request handler at 3am. A tiny guard turns a confusing runtime crash into a clear startup error:

JS
const required = ['DATABASE_URL', 'JWT_SECRET']
const missing = required.filter((key) => !process.env[key])

if (missing.length > 0) {
  console.error(`Missing required env vars: ${missing.join(', ')}`)
  process.exit(1)
}
Missing required env vars: JWT_SECRET
Schema validation scales better
For more than a handful of vars, validate and coerce with a schema library (`zod`, `envalid`). You get types, defaults, and numeric/boolean parsing in one place — and the rest of your app reads a clean, typed config object instead of poking `process.env` everywhere.
Next
Learn to respond to the OS and shutdown signals in [Process Events & Signals](/nodejs/process-events).