Home JavaScript Environment Variables in Node.js with dotenv
Beginner 6 min · 2026-07-12

Environment Variables in Node.js with dotenv

Environment variables in Node.js: dotenv setup, .env files, validation with Zod, production secrets management, and avoiding the common mistake of committing secrets to git..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 15-20 minutes
  • Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
 ● Production Incident
Quick Answer

Environment variables (process.env) are the standard way to configure Node.js applications across environments. The dotenv package loads variables from a .env file into process.env at application star

✦ Definition~90s read
What is Environment Variables in Node.js with dotenv?

Environment variables (process.env) are the standard way to configure Node.js applications across environments. The dotenv package loads variables from a .env file into process.env at application startup. Best practices include using .env.example with placeholder values, never committing .env files to version control, validating required variables at startup with libraries like Zod or Joi, and using secret managers (AWS Secrets Manager, HashiCorp Vault) for production secrets rather than .env files.

Think of environment variables like the settings on a restaurant's kitchen display screen.
Plain-English First

Think of environment variables like the settings on a restaurant's kitchen display screen. The chef doesn't hardcode the oven temperature or the specials menu into the recipe book; instead, they read them from a screen that can be changed without rewriting the recipes. Similarly, environment variables let your app read configuration (like database passwords or API keys) from the system, so you can change settings without touching the code.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

DATABASE_URL=postgres://localhost:5432/myapp — the line that, when accidentally committed to a public repo, costs companies millions in breached data. Environment variables are the most basic infrastructure decision in any Node.js application, yet teams consistently get them wrong: committing .env files, lacking validation, using the same config for development and production. This article covers the correct setup, validation patterns, production secrets management, and the one pattern that prevents the most common environment variable disaster.

Why Environment Variables Matter in Production

Hardcoding configuration values like database passwords, API keys, or secret tokens directly into your source code is a security risk and a maintenance nightmare. In production, you need to separate configuration from code to allow different settings across environments without modifying the codebase. Environment variables provide a standard way to inject configuration into your Node.js application at runtime. They keep secrets out of version control, enable per-environment overrides, and simplify deployment pipelines. The Twelve-Factor App methodology explicitly recommends storing config in environment variables. Without them, you risk exposing credentials in Git history, breaking deployments due to config mismatches, or accidentally shipping debug settings to production. This article walks you through using the dotenv package to manage environment variables safely and effectively.

.env.exampleBASH
1
2
3
4
5
6
7
8
9
# .env.example
# Copy this file to .env and fill in your values
DB_HOST=localhost
DB_PORT=5432
DB_USER=myapp
DB_PASS=changeme
API_KEY=your-api-key-here
NODE_ENV=development
⚠ Never commit .env files
Always add .env to your .gitignore. Only commit .env.example with placeholder values so other developers know which variables are needed.
📊 Production Insight
In production, we once had a junior dev commit a .env file with real AWS keys. Within hours, attackers spun up crypto-mining instances. Always use .gitignore and consider secret scanning tools.
🎯 Key Takeaway
Environment variables keep configuration out of code, preventing secret leaks and enabling environment-specific settings.
dotenv-environment-variables THECODEFORGE.IO Environment Variable Management Stack Layered architecture for secure configuration in Node.js Application Code process.env calls | Configuration modules dotenv Runtime config() method | Parsing .env files Environment Files .env | .env.development | .env.production Secrets Management .gitignore | Environment variables in CI/CD THECODEFORGE.IO
thecodeforge.io
Dotenv Environment Variables

Setting Up dotenv in Your Project

The dotenv package loads environment variables from a .env file into process.env. Install it as a production dependency: npm install dotenv. Then, at the very top of your application's entry point (e.g., app.js or index.js), add require('dotenv').config(). This reads the .env file and merges its key-value pairs into process.env. If you're using ES modules, use import 'dotenv/config' instead. The .env file should never be committed to version control; instead, provide a .env.example file with dummy values. For local development, each developer creates their own .env file. In production, you typically set environment variables through your hosting platform (e.g., Heroku config vars, AWS ECS environment, Docker compose) and skip the .env file entirely. This setup ensures your code works identically across all environments.

app.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app.js
require('dotenv').config();

const express = require('express');
const app = express();

const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send(`Hello from ${process.env.NODE_ENV || 'development'}`);
});

app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});
Output
Server running on port 3000
Try it live
💡Order matters
Call dotenv.config() as early as possible, before any other module that reads environment variables. Otherwise, those modules might see undefined values.
📊 Production Insight
We once had a staging outage because dotenv was loaded after a database connection module. The DB host was undefined, so it defaulted to localhost, which didn't exist in the container. Load dotenv first.
🎯 Key Takeaway
Install dotenv, load it at the entry point, and keep .env out of version control.

The .env File Format and Best Practices

The .env file uses a simple KEY=VALUE format. Each line defines one variable. Values can be quoted (single or double) if they contain spaces or special characters. Comments start with #. There's no need to export variables; dotenv handles that. Best practices: use uppercase with underscores for variable names (e.g., DB_HOST). Group related variables with comments. Never store sensitive values in .env.example. Use default values in code when a variable might be missing (e.g., const port = process.env.PORT || 3000). For boolean values, use strings like 'true' and 'false' and parse them explicitly. Avoid using .env in production; instead, set variables via the OS or container orchestration. If you must use a file in production, ensure it's outside the application root and has restricted permissions (e.g., chmod 600).

.envBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
# Database configuration
DB_HOST=prod-db.example.com
DB_PORT=5432
DB_USER=app_user
DB_PASS=s3cret!Pass

# Feature flags
ENABLE_CACHE=true
LOG_LEVEL=info

# Third-party keys
STRIPE_API_KEY=sk_live_...
🔥Variable expansion
dotenv does not support variable expansion like DB_URL=${DB_HOST}:${DB_PORT}. Use a library like dotenv-expand if you need that.
📊 Production Insight
A team once used a .env file in production with world-readable permissions (644). An attacker who gained limited access could read all secrets. Always restrict file permissions to owner-only (600) in production.
🎯 Key Takeaway
Use UPPER_CASE names, comments, and defaults. Keep .env out of production.
dotenv-environment-variables THECODEFORGE.IO Environment Variable Management Layers Hierarchical structure for secure configuration Application Code process.env | Config Module dotenv Library Parsing | Loading Environment Files .env | .env.development | .env.production Security Layer .gitignore | Encryption THECODEFORGE.IO
thecodeforge.io
Dotenv Environment Variables

Accessing Environment Variables in Code

After loading dotenv, you access variables via process.env.VARIABLE_NAME. This returns a string (or undefined if not set). Always validate required variables at startup. For example, if your app needs a database URL, check if it exists and crash early with a clear error message. Use defaults for optional variables. For numbers, parse them explicitly (e.g., parseInt(process.env.PORT, 10)). For booleans, compare the string value (e.g., process.env.DEBUG === 'true'). Never trust that a variable is set; always handle missing values gracefully. In production, you might want to use a validation library like joi or env-var to enforce types and required fields. This prevents runtime errors due to misconfiguration.

config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// config.js
require('dotenv').config();

const required = ['DB_HOST', 'DB_USER', 'DB_PASS', 'API_KEY'];
const missing = required.filter(key => !process.env[key]);
if (missing.length) {
  throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}

module.exports = {
  db: {
    host: process.env.DB_HOST,
    port: parseInt(process.env.DB_PORT, 10) || 5432,
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
  },
  apiKey: process.env.API_KEY,
  isProduction: process.env.NODE_ENV === 'production',
};
Try it live
⚠ Type coercion
All environment variables are strings. If you need a number or boolean, parse them explicitly. parseInt with radix 10 avoids octal interpretation.
📊 Production Insight
We had a production crash because process.env.PORT was set to '3000' but the code used +process.env.PORT which worked, until someone set PORT to 'abc' by accident. Always validate and parse with fallbacks.
🎯 Key Takeaway
Validate required variables at startup, parse types explicitly, and provide sensible defaults.

Managing Multiple Environments (Development, Staging, Production)

Different environments need different configurations. The simplest approach is to have separate .env files per environment, e.g., .env.development, .env.staging, .env.production. Then load the appropriate one based on NODE_ENV. You can use dotenv.config({ path: .env.${process.env.NODE_ENV} }). However, in production, you should not use .env files at all; set variables via the platform. For local development, you can have a .env file that overrides defaults. Another pattern is to use a single .env file with all variables, but override specific ones with environment-specific files. The key is consistency: your code should read from process.env regardless of how the variables were set. Use a configuration module that centralizes all environment variable access, making it easy to see what's expected.

config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// config.js
const dotenv = require('dotenv');
const path = require('path');

const env = process.env.NODE_ENV || 'development';

// Load environment-specific .env file if it exists
const envFile = path.resolve(__dirname, `../.env.${env}`);
dotenv.config({ path: envFile });

// Also load .env for local overrides (optional)
dotenv.config();

module.exports = {
  nodeEnv: env,
  port: process.env.PORT || 3000,
  dbUrl: process.env.DATABASE_URL,
};
Try it live
💡Order of precedence
dotenv does not overwrite existing environment variables. So if you set a variable in the shell, it takes precedence over .env files. Use this to override for testing.
📊 Production Insight
A common mistake is to accidentally load a .env file in production that overrides platform-set variables. We always ensure dotenv.config() is called only when NODE_ENV is not 'production', or we skip .env loading entirely in production.
🎯 Key Takeaway
Use environment-specific .env files locally, but rely on platform variables in production.

Security: Keeping Secrets Out of Source Control

The primary reason to use environment variables is security. Never commit secrets to Git. Add .env to .gitignore immediately. Use .env.example as a template with placeholder values. For teams, consider using a secrets manager like HashiCorp Vault, AWS Secrets Manager, or Doppler. These tools inject secrets at runtime, avoiding files altogether. If you must use files, encrypt them (e.g., with git-crypt or sops). Also, be careful with logging: never log environment variables directly. A common production incident is accidentally printing process.env in a debug log, exposing secrets in log aggregation tools. Use a sanitized logger that redacts sensitive fields. Finally, rotate secrets regularly and audit who has access.

.gitignoreBASH
1
2
3
4
5
# .gitignore
.env
.env.*
!env.example
⚠ Accidental commits
📊 Production Insight
A startup I consulted for had their entire AWS account compromised because a .env file with IAM keys was committed to a public repo. The keys were harvested within minutes. Always use IAM roles or short-lived credentials in production.
🎯 Key Takeaway
Never commit .env files. Use .gitignore, .env.example, and consider a secrets manager for production.

Testing with Environment Variables

Testing code that relies on environment variables can be tricky because process.env is global. You have a few options: (1) Set variables in your test runner's setup (e.g., Jest's setupFiles). (2) Use a library like dotenv with a test-specific .env.test file. (3) Mock process.env using a utility like jest.resetModules() to reload modules with different env values. The cleanest approach is to inject configuration via a config module that can be mocked. For example, instead of reading process.env.DB_HOST directly in your database module, read it from a config object that you can replace in tests. This makes your code testable without side effects. Always reset environment variables between tests to avoid leakage.

__tests__/config.test.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// __tests__/config.test.js

describe('config', () => {
  const OLD_ENV = process.env;

  beforeEach(() => {
    jest.resetModules();
    process.env = { ...OLD_ENV };
  });

  afterAll(() => {
    process.env = OLD_ENV;
  });

  test('should use default port when PORT is not set', () => {
    delete process.env.PORT;
    const config = require('../config');
    expect(config.port).toBe(3000);
  });

  test('should read PORT from environment', () => {
    process.env.PORT = '4000';
    const config = require('../config');
    expect(config.port).toBe(4000);
  });
});
Output
PASS __tests__/config.test.js
Try it live
🔥Reset process.env
Always save and restore process.env in tests to prevent state leakage between test files. Use jest.resetModules() to force re-evaluation of modules that cache config at load time.
📊 Production Insight
We once had a test suite that passed locally but failed in CI because a developer's .env file set a variable that the tests depended on. Always clear process.env in test setup to ensure isolation.
🎯 Key Takeaway
Mock environment variables in tests by resetting process.env and reloading modules.

Common Pitfalls and How to Avoid Them

Several mistakes plague developers new to dotenv. (1) Forgetting to call dotenv.config() before accessing variables. (2) Committing the .env file. (3) Using .env in production. (4) Assuming variables are always set. (5) Not parsing types (e.g., treating 'false' as truthy). (6) Overwriting existing environment variables (dotenv does not overwrite by default, but you can force it with { override: true }). (7) Loading dotenv in multiple places, causing confusion. (8) Using spaces around = in .env file (they become part of the key or value). (9) Expecting dotenv to expand variables. (10) Not handling missing .env file gracefully (dotenv doesn't throw if the file is missing, but you might want to warn). Avoid these by following the practices outlined in this article: load early, validate, type-check, and never commit secrets.

bad-example.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// BAD: Loading dotenv too late
const db = require('./db'); // reads process.env.DB_HOST before dotenv loaded
require('dotenv').config();

// BAD: Not parsing boolean
if (process.env.DEBUG) { // always true if set to 'false'
  console.log('debug mode');
}

// BAD: Using .env in production
if (process.env.NODE_ENV === 'production') {
  require('dotenv').config(); // don't do this
}
Try it live
⚠ Spaces around =
In .env files, KEY = VALUE will set the key to ' VALUE' (with leading space). Always use KEY=VALUE without spaces.
📊 Production Insight
A team spent hours debugging why process.env.FEATURE_FLAG was 'false' but the condition if (process.env.FEATURE_FLAG) still executed. They forgot that any non-empty string is truthy. Always compare to the string 'true'.
🎯 Key Takeaway
Load dotenv first, validate, parse types, and never use .env in production.

Alternatives to dotenv and When to Use Them

While dotenv is great for local development, production environments often need more robust solutions. (1) Platform-native variables: Heroku, AWS ECS, Docker Compose, and Kubernetes all support setting environment variables natively. (2) Secrets managers: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, and GCP Secret Manager provide encrypted storage and rotation. (3) Configuration services: Consul, etcd, or Spring Cloud Config for distributed systems. (4) Encrypted files: Use sops or git-crypt to encrypt .env files in the repo. (5) Environment variable validators: Libraries like env-var or joi enforce types and required fields. For most Node.js apps, start with dotenv for development and switch to platform variables for production. If you need to manage secrets across many services, invest in a secrets manager. The principle remains: separate config from code.

using-env-var.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
// using env-var for validation
const env = require('env-var');

const config = {
  port: env.get('PORT').default(3000).asPortNumber(),
  dbHost: env.get('DB_HOST').required().asString(),
  debug: env.get('DEBUG').default('false').asBoolStrict(),
};

module.exports = config;
Try it live
🔥dotenv is for dev only
In production, rely on the platform's environment variable mechanism. dotenv is a convenience for local development, not a production-grade secrets manager.
📊 Production Insight
We migrated from dotenv to AWS Secrets Manager for a PCI-compliant app. The transition required code changes to fetch secrets at startup, but it eliminated the risk of secret files on disk and gave us automatic rotation.
🎯 Key Takeaway
Use dotenv for local dev, platform variables for production, and secrets managers for sensitive data.

Putting It All Together: A Production-Ready Setup

Here's a complete, production-ready pattern. Your entry point loads dotenv only if not in production. A config module validates and exports typed variables. The rest of the app imports config, not process.env directly. This centralizes configuration, makes testing easy, and prevents scattered process.env calls. In production, you set variables via the platform (e.g., Docker environment, Kubernetes secrets). The .env file is never committed. Use .env.example as documentation. Add a startup check that crashes the app if required variables are missing. This pattern has served us well across dozens of Node.js services, from small APIs to microservices handling millions of requests.

index.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// index.js - entry point
if (process.env.NODE_ENV !== 'production') {
  require('dotenv').config();
}

const config = require('./config');
const app = require('./app');

app.listen(config.port, () => {
  console.log(`Server running in ${config.nodeEnv} on port ${config.port}`);
});

// config.js
const required = ['DATABASE_URL', 'API_KEY'];
const missing = required.filter(key => !process.env[key]);
if (missing.length) {
  throw new Error(`Missing required env vars: ${missing.join(', ')}`);
}

module.exports = {
  nodeEnv: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT, 10) || 3000,
  databaseUrl: process.env.DATABASE_URL,
  apiKey: process.env.API_KEY,
  isProduction: process.env.NODE_ENV === 'production',
};
Output
Server running in development on port 3000
Try it live
💡Fail fast
Crash on startup if required variables are missing. It's better to fail immediately than to fail mysteriously at runtime.
📊 Production Insight
We adopted this pattern after a production incident where a missing DATABASE_URL caused the app to start but fail on the first request, leading to a 5-minute outage. Now we fail fast and alert immediately.
🎯 Key Takeaway
Centralize config, validate at startup, load dotenv only in non-production, and never access process.env directly in business logic.

Node 20+ Built-in --env-file CLI Flag

Starting with Node 20, you can load environment variables from a file without any third-party library. The --env-file flag allows you to specify a path to a .env file when running your application. This is a zero-dependency solution for simple use cases. For example, node --env-file=.env server.js loads variables from .env into process.env. This approach is ideal for small projects or scripts where you want to avoid adding dotenv. However, it lacks features like variable expansion, override, or multi-file support. For production, you may still prefer dotenv for its flexibility and ecosystem. Note that --env-file is still experimental in Node 20, so use with caution in critical environments.

terminalBASH
1
2
# Run your app with .env file
node --env-file=.env server.js
Output
Server running on port 3000
🔥Experimental Feature
The --env-file flag is experimental in Node 20. It may change in future releases. For production, stick with dotenv until it's stable.
📊 Production Insight
For production apps, use dotenv for reliability and feature completeness. Reserve --env-file for quick prototypes or scripts.
🎯 Key Takeaway
Node 20+ provides a built-in way to load .env files via --env-file, but it's experimental and lacks advanced features.

process.loadEnvFile() and util.parseEnv()

Node 20 also introduces process.loadEnvFile() as a programmatic alternative to the CLI flag. This method loads a .env file into process.env at runtime. For example, process.loadEnvFile('.env') does the same as dotenv.config(). Additionally, util.parseEnv() parses a string of key-value pairs into an object, useful for custom parsing. These built-ins reduce dependency on dotenv for basic loading. However, they don't support variable expansion, override, or multi-file loading. If your project needs those features, stick with dotenv. The built-ins are best for minimal setups or when you want to avoid third-party code in a security-critical context.

app.jsJAVASCRIPT
1
2
3
4
5
6
7
8
import { parseEnv } from 'node:util';

// Load .env file
process.loadEnvFile('.env');

// Parse a custom string
const env = parseEnv('KEY=VALUE\nFOO=BAR');
console.log(env); // { KEY: 'VALUE', FOO: 'BAR' }
Output
{ KEY: 'VALUE', FOO: 'BAR' }
Try it live
⚠ Missing Features
process.loadEnvFile() does not support variable expansion (e.g., ${VAR}) or override. Use dotenv if you need those.
📊 Production Insight
Consider using these built-ins in serverless functions or microservices where minimizing dependencies is critical.
🎯 Key Takeaway
process.loadEnvFile() and util.parseEnv() are lightweight built-in alternatives to dotenv for basic .env loading.

dotenv-expand for Variable Expansion

Variable expansion allows you to reference other environment variables within your .env file using ${VAR} syntax. For example, DATABASE_URL=${HOST}:${PORT}/db. The dotenv-expand package adds this capability to dotenv. Install it with npm install dotenv-expand, then use dotenvExpand.expand(dotenv.config()). This is essential for reducing duplication and centralizing configuration. Without expansion, you'd have to hardcode values or use string concatenation in code. Note that expansion order matters: variables are expanded in the order they appear in the file. Use it wisely to avoid circular references. For production, always validate that expanded variables resolve correctly.

config.jsJAVASCRIPT
1
2
3
4
5
6
7
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';

const myEnv = dotenv.config();
dotenvExpand.expand(myEnv);

console.log(process.env.DATABASE_URL);
Output
localhost:5432/mydb
Try it live
💡Order Matters
Variables are expanded in file order. Define referenced variables before they are used.
📊 Production Insight
Use expansion to define base URLs or connection strings once, then reference them across multiple variables.
🎯 Key Takeaway
dotenv-expand enables variable expansion in .env files, reducing duplication and centralizing config.

DOTENV_CONFIG_ Env Config and Override Option

dotenv supports configuration via environment variables prefixed with DOTENV_CONFIG_. For example, DOTENV_CONFIG_PATH sets the path to the .env file, and DOTENV_CONFIG_DEBUG enables debug logging. This is useful for CI/CD pipelines or Docker containers where you want to override the default behavior without code changes. Additionally, dotenv's override option (available since v16) allows you to overwrite existing process.env values. By default, dotenv does not override existing variables. Set { override: true } in dotenv.config() to force overwrite. This is critical when you need to ensure that environment variables from a file take precedence over system-set ones, or vice versa. Use with caution to avoid accidental overwrites.

config.jsJAVASCRIPT
1
2
3
4
5
6
7
import dotenv from 'dotenv';

// Override existing env vars
dotenv.config({ override: true });

// Or use DOTENV_CONFIG_ env vars
// DOTENV_CONFIG_PATH=/custom/path/.env node app.js
Try it live
⚠ Override Risks
Overriding can mask system-set variables. Use only when you explicitly want file values to win.
📊 Production Insight
In Docker, set DOTENV_CONFIG_PATH to load a specific .env file per container, and use override to ensure file values take precedence.
🎯 Key Takeaway
DOTENV_CONFIG_ env vars and the override option give you fine-grained control over dotenv behavior.
Hardcoded vs Environment Variables Trade-offs in configuration management Hardcoded Values Environment Variables Security Secrets exposed in code Secrets kept out of source control Flexibility Requires code changes per environment Switch environments without code changes Maintainability Scattered across codebase Centralized in .env files Testing Difficult to mock Easy to override per test THECODEFORGE.IO
thecodeforge.io
Dotenv Environment Variables

Multi-File Loading Patterns

In complex projects, you may need to load multiple .env files, such as .env for defaults, .env.local for local overrides, and .env.production for production-specific values. dotenv supports this by calling dotenv.config() multiple times. The order matters: later calls override earlier ones. A common pattern is to load .env first, then .env.local (if it exists), and finally .env.${NODE_ENV}. This allows you to have a base configuration that can be overridden per environment. Use the path option to specify each file. For security, ensure that .env.local is in .gitignore. This pattern is widely used in frameworks like Next.js and is production-proven.

config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import dotenv from 'dotenv';
import { existsSync } from 'fs';

// Load base .env
dotenv.config({ path: '.env' });

// Load local overrides if exists
if (existsSync('.env.local')) {
  dotenv.config({ path: '.env.local', override: true });
}

// Load environment-specific file
const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
if (existsSync(envFile)) {
  dotenv.config({ path: envFile, override: true });
}
Try it live
💡Order of Precedence
Later files override earlier ones. Typically: .env (lowest) < .env.local < .env.<environment> (highest).
📊 Production Insight
Use this pattern to manage secrets per environment: .env for defaults, .env.production for production secrets (never committed).
🎯 Key Takeaway
Multi-file loading with dotenv allows environment-specific overrides while keeping a base config.
● Production incidentPOST-MORTEMseverity: high

The Case of the Missing .env in Production

Symptom
Service started but immediately exited with exit code 0. No error logs. Health checks failed.
Assumption
The team assumed dotenv would gracefully handle a missing .env file by just skipping it.
Root cause
dotenv.config() throws an error by default if the file is not found. The error was caught by a generic try-catch that logged nothing and exited.
Fix
Set dotenv.config({ path: '.env', override: true }) only in development, and conditionally load it based on NODE_ENV. In production, rely on Kubernetes secrets or environment variables set by the platform.
Key lesson
  • Never assume dotenv will silently fail; always handle missing files explicitly.
  • Use environment-specific configuration loading (e.g., if (process.env.NODE_ENV !== 'production') dotenv.config()).
  • Implement a startup validation that checks for required environment variables and fails loudly.
⚙ Quick Reference
15 commands from this guide
FileCommand / CodePurpose
.env.exampleDB_HOST=localhostWhy Environment Variables Matter in Production
app.jsrequire('dotenv').config();Setting Up dotenv in Your Project
.envDB_HOST=prod-db.example.comThe .env File Format and Best Practices
config.jsrequire('dotenv').config();Accessing Environment Variables in Code
config.jsconst dotenv = require('dotenv');Managing Multiple Environments (Development, Staging, Produc
.gitignore.envSecurity
__tests__config.test.jsdescribe('config', () => {Testing with Environment Variables
bad-example.jsconst db = require('./db'); // reads process.env.DB_HOST before dotenv loadedCommon Pitfalls and How to Avoid Them
using-env-var.jsconst env = require('env-var');Alternatives to dotenv and When to Use Them
index.jsif (process.env.NODE_ENV !== 'production') {Putting It All Together
terminalnode --env-file=.env server.jsNode 20+ Built-in --env-file CLI Flag
app.jsprocess.loadEnvFile('.env');process.loadEnvFile() and util.parseEnv()
config.jsconst myEnv = dotenv.config();dotenv-expand for Variable Expansion
config.jsdotenv.config({ override: true });DOTENV_CONFIG_ Env Config and Override Option
config.jsdotenv.config({ path: '.env' });Multi-File Loading Patterns

Key takeaways

1
Separate config from code
Use environment variables to keep secrets and environment-specific settings out of your source code, preventing accidental leaks and simplifying deployments.
2
Validate and type-check
Always validate required variables at startup, parse numbers and booleans explicitly, and provide sensible defaults to avoid runtime surprises.
3
Use dotenv only for development
In production, rely on platform-native environment variables or secrets managers. Never commit .env files or use them in production.
4
Centralize configuration access
Create a config module that reads from process.env and exports typed values. This makes testing easier, prevents scattered process.env calls, and enforces validation.
5
Node 20+ Built-in Support
Node 20 introduces --env-file CLI flag, process.loadEnvFile(), and util.parseEnv() for loading .env files without dotenv. These are experimental but useful for minimal setups.
6
dotenv-expand for Variable Expansion
Use dotenv-expand to reference other variables in your .env file with ${VAR} syntax, reducing duplication and centralizing configuration.
7
Multi-File Loading with Override
Load multiple .env files (e.g., .env, .env.local, .env.production) with dotenv's override option to create a layered configuration system that is environment-aware.
8
Node 20+ Built-in Features
Use --env-file, process.loadEnvFile(), and util.parseEnv() to reduce dependencies, but be aware they are experimental and lack advanced features like variable expansion.
9
dotenv-expand for DRY Config
Enable variable interpolation in .env files to avoid duplication, but ensure dependency order and consider using processEnv option to include system variables.
10
Multi-File Loading Best Practices
Layer .env files (base, environment, local) with careful ordering and gitignore strategy. In production, prefer a single managed .env file.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the purpose of the dotenv package?
Q02JUNIOR
How do you ensure .env files are not committed to version control?
Q03SENIOR
What happens if you call dotenv.config() after your application has alre...
Q04SENIOR
How would you handle multiple environments (dev, staging, prod) with dot...
Q05SENIOR
What security risks are associated with using dotenv in production?
Q06SENIOR
How would you implement a fallback mechanism for required environment va...
Q01 of 06JUNIOR

What is the purpose of the dotenv package?

ANSWER
dotenv loads environment variables from a .env file into process.env, allowing you to separate configuration from code, especially in development.
FAQ · 12 QUESTIONS

Frequently Asked Questions

01
What is dotenv and why should I use it?
02
How do I prevent my .env file from being committed to Git?
03
Can I use dotenv in production?
04
How do I handle boolean environment variables correctly?
05
What's the best way to test code that uses environment variables?
06
How do I manage secrets across multiple environments and services?
07
Can I use dotenv with Node 20's built-in --env-file?
08
How do I handle circular references in dotenv-expand?
09
What is the difference between DOTENV_CONFIG_PATH and the path option in dotenv.config()?
10
How do I use Node 20's --env-file flag with multiple .env files?
11
What is the difference between process.loadEnvFile() and dotenv.config()?
12
Can I use dotenv-expand with Node 20's built-in --env-file?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's Node.js. Mark it forged?

6 min read · try the examples if you haven't

Previous
Debugging Node.js Applications — Inspector, Chrome DevTools, and VS Code
22 / 47 · Node.js
Next
CORS in Node.js and Express — The Complete Guide