惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 司徒正美
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
Martin Fowler
Martin Fowler
罗磊的独立博客
The GitHub Blog
The GitHub Blog
L
LangChain Blog
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
DataBreaches.Net
宝玉的分享
宝玉的分享
U
Unit 42
阮一峰的网络日志
阮一峰的网络日志
Last Week in AI
Last Week in AI
N
Netflix TechBlog - Medium
The Cloudflare Blog
Microsoft Azure Blog
Microsoft Azure Blog
H
Help Net Security
美团技术团队
大猫的无限游戏
大猫的无限游戏
雷峰网
雷峰网
爱范儿
爱范儿
酷 壳 – CoolShell
酷 壳 – CoolShell
MongoDB | Blog
MongoDB | Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Environment Variables in Node.js: The Complete Guide (2026)
Alex Chen · 2026-05-30 · via DEV Community

Alex Chen

Environment Variables in Node.js: The Complete Guide (2026)

Environment variables are the standard way to configure apps across environments. Here's how to use them correctly.

What and Why

What: Key-value pairs set outside your application code
Where: OS environment, .env files, CI/CD config, container orchestration
Why:
→ Separate config from code (12-Factor App methodology)
→ Same code runs everywhere (dev, staging, production)
→ Secrets never committed to git
→ Easy to change behavior without redeploying

Reading Env Vars in Node.js

// Method 1: process.env (built-in, always available)
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const apiKey = process.env.API_KEY;

// ⚠️ process.env values are ALWAYS strings!
const timeout = parseInt(process.env.TIMEOUT, 10) || 5000;
const debug = process.env.DEBUG === 'true';
const maxRetries = Number(process.env.MAX_RETRIES || '3');

// Method 2: dotenv (most popular approach)
// npm install dotenv
import 'dotenv/config'; // Loads .env into process.env automatically
// Now process.env has all your .env variables!

// Or explicit load:
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' }); // Custom file path

// Method 3: env-cmd (for package.json scripts)
// "dev": "env-cmd -f .env.dev node server.js"
// "prod": "env-cmd -f .env.prod node server.js"

// Method 4: tenv / enve (type-safe alternatives)
import { env } from 'tenv';
const port = env.number('PORT', 3000); // Type-safe with default
const dbUrl = env.string('DATABASE_URL'); // Required, throws if missing
const debug = env.bool('DEBUG', false); // Boolean parsing

The .env File Ecosystem

# .env (committed to git with defaults)
NODE_ENV=development
PORT=3000
LOG_LEVEL=debug

# .env.local (NOT committed! Gitignored)
# Contains local overrides and secrets
DATABASE_URL=postgresql://localhost/myapp_dev
API_KEY=sk_test_local_key
JWT_SECRET=local-dev-secret-not-for-production

# .env.production (NOT committed!)
# Production values loaded only on production servers
DATABASE_URL=postgresql://prod-server:5432/prod_db
API_KEY=sk_live_production_key
JWT_SECRET=super-secure-random-string-here
REDIS_URL=redis://prod-redis:6379

# .env.test (for test suite)
NODE_ENV=test
DATABASE_URL=sqlite::memory:
LOG_LEVEL=error

Best Practices

Structure & Organization

# Directory structure for larger projects:
config/
  index.ts          # Exports validated config object
  database.ts       # DB connection settings
  auth.ts           # Auth-related config
  redis.ts          # Redis/cache config
  email.ts          # Email service config
  validation.ts     # Zod schemas for type safety

# .env.example (COMMITTED to git!)
# Template showing all required variables (no real values):
# Copy this to .env.local and fill in your values
cp .env.example .env.local

# .env.example contents:
# === Database ===
DATABASE_URL=postgresql://user:pass@localhost:5432/dbname
# === Server ===
PORT=3000
NODE_ENV=development
# === Auth ===
JWT_SECRET=generate-with:openssl rand -base64 32
JWT_EXPIRES_IN=15m
# === External APIs ===
OPENAI_API_KEY=sk-...
# === Redis (optional) ===
REDIS_URL=redis://localhost:6379

Validation at Startup

// config/index.ts — Validate ALL env vars when app starts
import z from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'staging', 'production']).default('development'),
  PORT: z.coerce.number().default(3000),

  DATABASE_URL: z.string().url('Must be a valid DATABASE_URL'),
  REDIS_URL: z.string().optional(),

  JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
  JWT_EXPIRES_IN: z.string().default('15m'),

  API_KEY: z.string().min(1).optional(),

  LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug']).default('info'),
  CORS_ORIGIN: z.string().default('http://localhost:3000'),

  RATE_LIMIT_WINDOW_MS: z.coerce.number().default(900000),
  RATE_LIMIT_MAX: z.coerce.number().default(100),
});

export type Env = z.infer<typeof envSchema>;

function validateEnv(): Env {
  const result = envSchema.safeParse(process.env);

  if (!result.error) {
    return result.data; // All good!
  }

  // Pretty error message listing missing/invalid vars
  const issues = result.error.issues.map(i => `  ${i.path.join('.')}: ${i.message}`);
  console.error('❌ Invalid environment variables:\n' + issues.join('\n'));
  console.error('\nCopy .env.example to .env.local and fill in the values.');
  process.exit(1); // Crash fast — don't run with bad config!
}

export const config = validateEnv();

// Now import config everywhere — fully typed, guaranteed valid!
import { config } from './config/index.js';
console.log(`Server starting on port ${config.PORT}`); // TypeScript knows it's a number
console.log(`DB: ${config.DATABASE_URL}`);             // Validated URL string

Security Rules

# .gitignore MUST include:
.env
.env.local
.env.*.local
!.env.example          # Commit the template, not real values

# Never:
❌ Commit .env files with real secrets
❌ Log environment variables (especially secrets)
❌ Read .env files after build (they should be resolved at startup)
❌ Use fallback secrets like "secret" or "password"
❌ Store secrets in source code (even "just for testing")

# Always:
✅ Use different secrets per environment
✅ Generate secrets with proper entropy:
   openssl rand -base64 32    # For JWT secrets
   openssl rand -hex 32        # For API keys/tokens
✅ Rotate secrets regularly
✅ Use a secrets manager in production (AWS SM, Vault, etc.)
✅ Set file permissions: chmod 600 .env.local

Advanced Patterns

// Pattern 1: 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',
};

if (features.newDashboard) {
  app.use('/dashboard', dashboardRouter);
}

// Pattern 2: Multi-database switching
const dbConfig = {
  sqlite: { client: 'sqlite3', connection: { filename: './data.db' } },
  postgresql: { client: 'pg', connection: process.env.DATABASE_URL },
};
const db = knex(dbConfig[process.env.DB_TYPE || 'sqlite']);

// Pattern 3: Graceful degradation
const optionalServices = {
  redis: process.env.REDIS_URL ? createRedisClient(process.env.REDIS_URL) : null,
  email: process.env.SENDGRID_API_KEY ? new SendGrid(process.env.SENDGRID_API_KEY) : null,
};

// Code that handles missing services:
async function cacheData(key, value) {
  if (optionalServices.redis) {
    await optionalServices.redis.setex(key, 3600, JSON.stringify(value));
  }
  // Continue without caching if Redis isn't configured
}

// Pattern 4: Environment-specific module loading
let logger;
switch (process.env.NODE_ENV) {
  case 'production':
    logger = (await import('./logger/winston.js')).default;
    break;
  case 'test':
    logger = (await import('./logger/silent.js')).default;
    break;
  default:
    logger = (await import('./logger/console.js')).default;
}

Docker & CI/CD

# Dockerfile — pass env vars at runtime, NOT build time
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
# DON'T: ENV DATABASE_URL=... here (secrets in image = bad)
CMD ["node", "dist/index.js"]

# docker-compose.yml
services:
  app:
    build: .
    ports: ["3000:3000"]
    env_file:
      - .env.local            # Load from file
    environment:
      - NODE_ENV=production   # Or inline override
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: ${DB_USER:-app}        # From .env or default
      POSTGRES_PASSWORD: ${DB_PASSWORD:?err} # Error if not set!

  redis:
    image: redis:7-alpine

# GitHub Actions (.github/workflows/ci.yml)
env:
  NODE_ENV: test
  DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
  CI: true

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - env:
          DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb
        run: npm test

Quick Reference

Common env var naming conventions:

Server:
  PORT, HOST, NODE_ENV, URL, BASE_URL, PUBLIC_URL

Database:
  DATABASE_URL, DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
  REDIS_URL, CACHE_URL, MONGO_URI

Auth:
  JWT_SECRET, JWT_EXPIRES_IN, SESSION_SECRET
  OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET
  API_KEY, API_SECRET, ENCRYPTION_KEY

External Services:
  STRIPE_KEY, SENDGRID_API_KEY, OPENAI_API_KEY
  AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
  SENTRY_DSN, DATADOG_API_KEY

Feature Flags:
  FEATURE_* prefix for all feature toggles

App-specific:
  APP_NAME, LOG_LEVEL, DEBUG, VERBOSE
  CORS_ORIGIN, ALLOWED_ORIGINS
  RATE_LIMIT_*, TIMEOUT_*, MAX_*


How do you manage environment variables in your projects? Dotenv or something else?

Follow @armorbreak for more practical developer guides.