How to Use Environment Variables in Node.js: process.env, .env files, and secure secrets
Environment variables are the standard way to configure a Node.js app without hardcoding values. Node reads them through process.env, you can load them from .env files with the built-in --env-file flag or the dotenv package, and secrets should stay out of your code and version control. This guide walks through each approach and when to use it.
On this page
- What process.env is
- Setting variables in the shell
- Reading .env files
- Option 1: Node's built-in --env-file (Node.js 20.6+)
- Option 2: the dotenv package
- .env file format rules
- Why secrets must not go in code
- Environment variables in deployment and CI
- FAQ
- Should I use --env-file or the dotenv package?
- Are environment variables secure?
- What is the difference between process.env and a .env file?
- How do I set environment variables in production?
- Verdict
Environment variables are key-value pairs that live outside your application code and configure how it runs in different situations. In Node.js you read them through process.env, which is an object pre-populated with your user's environment variables. Values you need across environments and should not hardcode — database URLs, API keys, ports, feature flags — belong in environment variables rather than in the source code. This guide shows how to read them, how to load them from .env files, and how to keep real secrets out of your repository.
What process.env is
In Node.js, process.env is a property that returns an object containing the user environment — the set of key-value variables the node process inherits from the shell or the environment you started it in. You read a value simply by accessing the property name:
console.log(process.env.NODE_ENV);
// "development" if you set NODE_ENV=development before starting the process
You can also set a value at runtime by assigning a property:
process.env.MY_VAR = "value";
In older Node.js versions assigning a non-string value was implicitly converted to a string; that behavior is deprecated and future versions may throw when the value is not a string, number, or boolean. Use delete process.env.KEY to remove a property.
Environment variables are not namespaced per app — they are inherited from the process that launched Node. That is exactly why they are good for configuration that differs between your laptop and production: the same code reads process.env.DB_URL, and each environment supplies its own value.
Setting variables in the shell
Before the process starts, you set variables in the environment. In a POSIX shell:
NODE_ENV=production node app.js
or export it for the current session:
export DB_URL=postgres://localhost:5432
node app.js
Values set this way are visible to the process through process.env. For local development, however, exporting dozens of values by hand is tedious, which is why .env files exist.
Reading .env files
A .env file (also called a dotenv file) is a plain-text list of KEY=VALUE pairs that define environment variables an application can load. The format was popularized by the dotenv package, and modern Node.js can read these files natively without any dependency.
Option 1: Node's built-in --env-file (Node.js 20.6+)
Since Node.js 20.6 you can load a .env file directly from the command line with no package install. If you have a .env file:
DB_URL=postgres://localhost:5432
PORT=8080
start the process with:
node --env-file=.env app.js
The variables in the file populate process.env, so your code reads them the same way as any other environment variable. If the same variable is defined both in the shell environment and in the file, the value from the environment takes precedence. You can pass multiple --env-file arguments, and later files override variables set by earlier ones; --env-file-if-exists is the variant that silently skips a missing file instead of failing.
Node.js also exposes two programmatic helpers: process.loadEnvFile(path) loads a file and populates process.env, and util.parseEnv() parses raw .env content into an object.
Option 2: the dotenv package
The dotenv package loads a .env file at runtime. Install it, call config() early in your entry file, and process.env is populated:
npm install dotenv
require("dotenv").config();
console.log(process.env.DB_URL);
You can point it at a custom path with dotenv.config({ path: "/custom/path/to/.env" }). dotenv is a fine choice when you want the loader inside your code or need to support older Node.js versions, but with a modern Node.js release the built-in --env-file flag removes the dependency entirely.
.env file format rules
The .env format follows a few simple rules, and both Node's implementation and dotenv honor them:
- Variable names must match
^[a-zA-Z_]+[a-zA-Z0-9_]*$— start with a letter or underscore, then letters, digits, or underscores.MY_VAR,my_var, andMy_Var123are valid;1_VAR,my-var,my var, and trailingVAR_are not. - Values are arbitrary text and may be wrapped in single or double quotes. Quoted values can span multiple lines; unquoted ones are single-line. Values are always parsed as text —
0,true, and{"hello":"world"}all become the literal strings"0","true", and'{"hello":"world"}', not a number, boolean, or object. - Spacing around keys and values is ignored unless enclosed in quotes.
- Comments start with
#and run to the end of the line; a#inside quotes is a normal character. exportprefixes are allowed and ignored — useful when you also want tosourcethe same file in a shell.
Example:
# application config
NODE_ENV=development
DB_URL="postgres://localhost:5432"
# a value with a literal hashtag must be quoted
API_TOKEN='abc#123'
Why secrets must not go in code
Environment variables solve a real security problem: configuration and credentials should not be committed to source control. Hardcoded values in code end up in your repository history, where anyone with access — and anyone who consumes a leaked snapshot — can read them. A .env file that contains real secrets should be listed in .gitignore so it is never committed, while a committed .env.example file documents which variables the app needs, with placeholder values.
Separating secrets from code also makes rotation possible without a redeploy. When a key leaks or expires, you update the variable in the deployment platform and restart, instead of editing source and shipping a new build. Managed secret stores — a secrets manager such as 1Password or a purpose-built store such as HashiCorp Vault — take this further by injecting values into the runtime environment or into deployment pipelines, which is the approach most mature teams move toward.
Environment variables in deployment and CI
The same code that reads process.env in development reads it in production. Hosting platforms let you set environment variables in their dashboard or config rather than in code, and CI/CD systems expose them as encrypted secrets. In a CI pipeline such as GitHub Actions, you define secrets in the repository settings and reference them in the workflow, so keys never appear in the workflow file or the log. Containerized apps follow the same principle — tools like Docker pass environment variables into containers, keeping runtime configuration out of the image.
FAQ
Should I use --env-file or the dotenv package?
Use Node.js's built-in --env-file flag (Node.js 20.6+) when you can pick the Node.js version — it needs no dependency and is the most direct option. Use the dotenv package when you need to load the file from inside your code, call it at a specific point at runtime, or must support an older Node.js version. Both read the same .env format. The native flag also has the advantage that the loader runs before your application code, so configuration is ready from the first line.
Are environment variables secure?
Environment variables keep secrets out of your source code and out of version control, which is a meaningful improvement — but they are not a complete security boundary. Anyone who can run code or read memory on the host can read them, and they can leak through logging or client-side exposure if you ship them to the browser. Never hardcode or log secrets, keep real ones out of committed .env files (.env should be in .gitignore), and use a dedicated secrets manager for the most sensitive credentials.
What is the difference between process.env and a .env file?
process.env is the live object Node exposes, pre-populated from the environment the process was launched in. A .env file is just a convenient on-disk format for declaring those variables so they can be loaded (via --env-file or dotenv) instead of being typed by hand. Loading a .env file populates process.env; it does not create a separate store.
How do I set environment variables in production?
You do not read or set them in your application code. Instead, configure them in your hosting platform's dashboard, config file, or deployment manifest, and for CI define them as encrypted repository secrets. The running process receives them just like local variables, so your code reads process.env unchanged across every environment.
Verdict
Use process.env to read configuration, and prefer Node.js's native --env-file flag (Node.js 20.6+) for loading local .env files so you need no extra dependency — or reach for dotenv if you must load from inside your code on an older runtime. Keep every real secret out of source control: put .env in .gitignore, commit an .env.example of placeholders, and let your deployment platform or a secrets manager like 1Password or HashiCorp Vault inject production values into the environment. The rule that makes all of it safe is simple: configuration belongs in the environment, never in the code.
Sources
- Node.js docs — Environment Variables (nodejs.org/api/environment_variables.html
- MIT)
- Node.js docs — process.env (nodejs.org/api/process.html#processenv
- MIT)
- Node.js blog — v20.6.0 (nodejs.org/en/blog/release/v20.6.0
- MIT)
- dotenv package (www.npmjs.com/package/dotenv
- BSD-2-Clause)
Share this guide
Related guides
- How to implement rate limiting in Node.js: algorithms and practical examples
Rate limiting protects Node.js APIs from abuse and overload. This guide explains fixed window, sliding window, and token bucket algorithms, shows how to add rate limiting to an Express API, and how to test it.
- How to handle errors in Node.js: try/catch, error-first callbacks, promises, and event emitters
Node.js errors come in several asynchronous styles, and each has its own correct way to be caught. This guide explains try/catch for synchronous code, error-first callbacks, promise rejection handling, and the 'error' event for streams and event emitters.
- How to add JWT authentication to a Node.js API: a practical guide
Step-by-step implementation of JWT authentication on a Node.js/Express API: password hashing, login endpoint, token verification middleware, refresh tokens, and the common security pitfalls.