正在加载,请稍候…

Environment Variables and Secrets Management: The Complete Production Guide

Learn how to properly manage environment variables and secrets across development, staging, and production. Covers .env files, vault solutions, Docker secrets, Kubernetes secrets, and common mistakes.

The Problem with Hardcoded Secrets

Every year, thousands of credentials get leaked on GitHub because someone hardcoded a database password, API key, or private key directly in source code. The fix sounds simple — "just use environment variables" — but the actual implementation across multiple environments and team members is where most projects go wrong.

This guide covers everything from basic .env files to production-grade secrets management.

The Twelve-Factor App: Config Principle

The Twelve-Factor methodology says: "Store config in the environment." Anything that varies between deployments (dev/staging/prod) belongs in environment variables:

  • Database connection strings
  • API keys and tokens
  • OAuth client secrets
  • Third-party service credentials
  • Feature flags
  • Port numbers and hostnames

Code that doesn't vary between environments (business logic, algorithms) belongs in the repository.

.env Files: Development Basics

# .env (development only — NEVER commit to git)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379
API_KEY=dev-key-not-real
JWT_SECRET=development-secret-change-in-production
STRIPE_SECRET_KEY=sk_test_...

# .env.example (ALWAYS commit — shows what variables are needed)
NODE_ENV=
PORT=3000
DATABASE_URL=
REDIS_URL=
API_KEY=
JWT_SECRET=
STRIPE_SECRET_KEY=
# .gitignore — absolutely required
.env
.env.local
.env.*.local
.env.production
# But NOT .env.example

Loading .env in Node.js

// Using dotenv (most common)
import 'dotenv/config'; // ES modules — loads .env automatically

// Or manually:
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' }); // Specify custom path

// Access variables
const dbUrl = process.env.DATABASE_URL;
const port = parseInt(process.env.PORT ?? '3000', 10);

Validating Environment Variables (Critical!)

// Use zod to validate and type environment variables
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'staging', 'production']),
  PORT: z.string().regex(/^d+$/).transform(Number).default('3000'),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  REDIS_URL: z.string().url().optional(),
});

// Parse and validate on startup — fail fast if misconfigured
const env = envSchema.safeParse(process.env);

if (!env.success) {
  console.error('❌ Invalid environment variables:');
  console.error(env.error.flatten().fieldErrors);
  process.exit(1);
}

// Export typed config
export const config = env.data;
// Now: config.PORT is number, config.DATABASE_URL is string, etc.

Failing fast on startup is far better than mysterious runtime errors when a missing variable is first accessed.

Environment-Specific Files

Different frameworks handle multiple environments differently:

# Common convention (Vite, Create React App, Next.js)
.env                # Base defaults (all environments)
.env.local          # Local overrides (gitignored)
.env.development    # Development-specific
.env.test           # Test-specific
.env.production     # Production (committed but no secrets!)

# Loading order (Vite):
# .env → .env.[mode] → .env.local → .env.[mode].local
# Later files override earlier ones
# In package.json scripts:
{
  "scripts": {
    "dev": "NODE_ENV=development node server.js",
    "test": "NODE_ENV=test jest",
    "start": "NODE_ENV=production node server.js"
  }
}

Docker: Environment Variables

# Dockerfile — never hardcode secrets
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

# ✅ Reference env vars with defaults
ENV NODE_ENV=production
ENV PORT=3000
# Don't set secrets here — pass them at runtime

EXPOSE $PORT
CMD ["node", "server.js"]
# docker-compose.yml (development)
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - PORT=3000
      # Reference from host environment:
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    env_file:
      - .env  # Load entire .env file

Docker Secrets (Swarm/Production)

# Create a secret
echo "my-super-secret-password" | docker secret create db_password -

# In docker-compose for Swarm:
services:
  app:
    image: myapp
    secrets:
      - db_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    external: true

# In your app, read the secret file:
const dbPassword = fs.readFileSync(process.env.DB_PASSWORD_FILE, 'utf8').trim();

Kubernetes Secrets

# Create a secret (base64 encoded)
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:  # stringData auto-base64-encodes
  database-url: "postgresql://user:pass@postgres:5432/myapp"
  jwt-secret: "your-production-jwt-secret"
  stripe-key: "sk_live_..."
---
# Reference in Deployment
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: app
          envFrom:
            - secretRef:
                name: app-secrets
          # Or individual env vars:
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: database-url
# Create secrets from command line (preferred — avoids YAML files with secrets)
kubectl create secret generic app-secrets   --from-literal=database-url="postgresql://..."   --from-literal=jwt-secret="$(openssl rand -hex 32)"

# View secrets (base64 encoded)
kubectl get secret app-secrets -o yaml

# Decode a specific value
kubectl get secret app-secrets -o jsonpath='{.data.jwt-secret}' | base64 -d

HashiCorp Vault: Enterprise Secrets Management

For large teams and enterprises, Vault provides centralized secrets management with audit trails:

# Start Vault dev server (development only)
vault server -dev

export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='dev-only-token'

# Store secrets
vault kv put secret/myapp   database_url="postgresql://..."   jwt_secret="$(openssl rand -hex 32)"

# Read secrets
vault kv get secret/myapp
vault kv get -field=jwt_secret secret/myapp
// Read from Vault in Node.js
import vault from 'node-vault';

const client = vault({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR,
  token: process.env.VAULT_TOKEN,  // Or use AppRole/K8s auth
});

const { data } = await client.read('secret/data/myapp');
const { database_url, jwt_secret } = data.data;

GitHub Actions / CI Secrets

# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}
          STRIPE_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
        run: |
          # These variables are masked in logs
          npm run deploy

Set secrets in: Repository → Settings → Secrets and variables → Actions

Cloud Provider Secret Managers

// AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({ region: 'us-east-1' });

async function getSecret(secretName) {
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: secretName })
  );
  return JSON.parse(response.SecretString);
}

const { database_url, jwt_secret } = await getSecret('myapp/production');
// Google Cloud Secret Manager
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';

const client = new SecretManagerServiceClient();

async function getSecret(name) {
  const [version] = await client.accessSecretVersion({
    name: `projects/MY_PROJECT/secrets/${name}/versions/latest`
  });
  return version.payload.data.toString();
}

Security Checklist

# Audit your repository for leaked secrets
git log --all --full-history -- "*.env"
git grep -i "password|secret|key|token" -- "*.ts" "*.js" "*.json"

# Use git-secrets to prevent committing secrets
brew install git-secrets
git secrets --install
git secrets --register-aws

# Detect secrets in existing codebase
pip install detect-secrets
detect-secrets scan --baseline .secrets.baseline

# Rotate secrets if you find a leak:
# 1. Revoke the compromised credential immediately
# 2. Generate a new credential
# 3. Update all deployments
# 4. Audit logs for unauthorized access
# 5. Consider git history rewrite (git-filter-repo)

Summary: Right Tool for Each Environment

Environment Solution
Local Dev .env file (gitignored)
CI/CD Platform secrets (GitHub Actions, GitLab CI)
Docker Dev env_file in docker-compose
Docker Prod Docker Secrets or mounted volumes
Kubernetes Kubernetes Secrets + Sealed Secrets
Cloud AWS Secrets Manager / GCP Secret Manager
Enterprise HashiCorp Vault

→ Generate secure random secrets with the Token Generator.