DEV Community

Alex Chen
Alex Chen

Posted on

Environment Variables in Node.js: The Complete Guide

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

dotenv — The Standard Way

npm install dotenv
Enter fullscreen mode Exit fullscreen mode
// 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',
});
Enter fullscreen mode Exit fullscreen mode

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'),
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
// Load based on NODE_ENV
require('dotenv').config({
  path: `.env.${process.env.NODE_ENV || 'development'}`,
});

// Fallback chain: .env.production → .env → process.env
Enter fullscreen mode Exit fullscreen mode

4. Security Rules

# NEVER commit these files!
.env
.env.local
.env.*.local
*.env
!.env.example         # Only commit the example/template
Enter fullscreen mode Exit fullscreen mode
// ❌ 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}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  });
}
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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)