Environment Variables in Node.js: The Complete Guide
Stop hardcoding secrets. Master env vars once and for all.
The Basics
# .env file (NEVER commit this!)
DATABASE_URL=postgres://localhost/myapp
JWT_SECRET=super_secret_key_123
API_KEY=sk_live_abc123
PORT=3000
NODE_ENV=production
# Rules:
# - No quotes needed (but allowed)
# - No spaces around =
# - Comments start with #
# - Empty lines ignored
Reading Env Vars in Node.js
// Built-in (always available)
process.env.PORT; // "3000"
process.env.NODE_ENV; // "production"
process.env.HOME; // "/root"
// ⚠️ Always strings! Even numbers are strings.
const port = Number(process.env.PORT) || 3000;
const debug = process.env.DEBUG === 'true';
const timeout = parseInt(process.env.TIMEOUT, 10) || 5000;
dotenv — The Standard Way
npm install dotenv
// Load as early as possible (top of your entry file)
require('dotenv').config();
// Now all vars from .env are in process.env!
console.log(process.env.DATABASE_URL); // Works!
// With options
require('dotenv').config({
path: '.env.production', // Custom env file
override: true, // Override existing vars?
encoding: 'utf8',
});
Best Practices
1. Validation at Startup
function requireEnv(key) {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value;
}
function optionalEnv(key, defaultValue = '') {
return process.env[key] || defaultValue;
}
// Config object — use throughout app
export const config = {
port: optionalEnv('PORT', '3000'),
nodeEnv: requireEnv('NODE_ENV'),
dbUrl: requireEnv('DATABASE_URL'),
jwtSecret: requireEnv('JWT_SECRET'),
apiKey: optionalEnv('API_KEY', ''),
isDev: process.env.NODE_ENV !== 'production',
logLevel: optionalEnv('LOG_LEVEL', 'info'),
};
2. Type-Safe Config with Zod
import { z } from 'zod';
const configSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
API_KEY: z.string().optional(),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
const config = configSchema.parse(process.env);
// Fully typed! config.PORT is number, config.NODE_ENV is union type
3. Multiple Environments
project/
├── .env # Shared defaults (committed)
├── .env.local # Local overrides (.gitignore)
├── .env.development # Dev specific (.gitignore)
├── .env.production # Prod specific (.gitignore)
├── .env.test # Test specific (.gitignore)
└── .env.example # Template for new developers (committed)
// Load based on NODE_ENV
require('dotenv').config({
path: `.env.${process.env.NODE_ENV || 'development'}`,
});
// Fallback chain: .env.production → .env → process.env
4. Security Rules
# NEVER commit these files!
.env
.env.local
.env.*.local
*.env
!.env.example # Only commit the example/template
// ❌ DANGER: Don't expose env vars to client!
app.get('/api/config', (req, res) => {
res.json({
// DON'T send: database url, jwt secret, api keys
// DO send only safe values:
appName: 'MyApp',
version: '1.0.0',
});
});
// Check for leaked secrets before deploy
const dangerousKeys = ['SECRET', 'KEY', 'PASSWORD', 'TOKEN', 'CREDENTIAL'];
for (const key of Object.keys(process.env)) {
if (dangerousKeys.some(d => key.includes(d))) {
console.warn(`⚠️ Sensitive env var detected: ${key}`);
}
}
Advanced Patterns
Encrypted Env Vars
// For production: encrypt sensitive values
const crypto = require('crypto');
function encrypt(text, masterKey) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(masterKey), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag().toString('hex');
return `${iv.toString('hex')}:${tag}:${encrypted}`;
}
// .env.production contains encrypted values
// Decrypt at startup using a master key from KMS / vault
Feature Flags via Env Vars
const features = {
newDashboard: process.env.FEATURE_DASHBOARD === 'true',
darkModeDefault: process.env.FEATURE_DARK_MODE === 'true',
betaApi: process.env.FEATURE_BETA_API === 'true',
maxUploadSize: Number(process.env.MAX_UPLOAD_MB) || 5,
};
if (features.newDashboard) {
// Serve new dashboard component
}
Dynamic Reload (Development)
// Watch .env changes during development
if (process.env.NODE_ENV === 'development') {
const chokidar = require('chokidar');
chokidar.watch('.env').on('change', () => {
console.log('.env changed, reloading...');
// Clear cached values
delete process.cache[require.resolve('dotenv')];
require('dotenv').config({ override: true });
// Trigger re-initialization of services that depend on env vars
});
}
Docker & Env Vars
# Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Don't copy .env! Pass at runtime instead
CMD ["node", "server.js"]
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
env_file:
- .env.production # Load from file
environment:
- NODE_ENV=production # Or inline (takes priority)
# Or use Docker secrets for sensitive data
secrets:
- jwt_secret
- db_password
secrets:
jwt_secret:
file: ./secrets/jwt_secret.txt
db_password:
file: ./secrets/db_password.txt
Quick Reference
| Method | Use Case |
|---|---|
process.env.X |
Read any env var |
dotenv.config() |
Load from .env file |
Number(process.env.X) |
Convert to number |
X === 'true' |
Convert to boolean |
.env.example |
Template for team |
zod validation |
Type-safe + validate |
Docker env_file
|
Container env vars |
secrets: |
Sensitive data in Docker |
How do you manage environment variables? Any tips?
Follow @armorbreak for more Node.js content.
Top comments (0)