Difficulty: Beginner
What are environment variables? How do you manage configuration and secrets across different environments?
Environment variables are key-value pairs set outside your application code that configure its behavior. They separate configuration from code, following the 12-Factor App principle that config should be stored in the environment.
Why environment variables? - Different values for dev/staging/production (database URLs, API keys) - Secrets never appear in source code or version control - Same code artifact runs in any environment by changing env vars - Easy to change without redeploying code
The dotenv pattern: a .env file in your project root holds local development values. Libraries like dotenv load these into process.env at startup. The .env file is in .gitignore, and each developer has their own copy. A .env.example file (committed to Git) documents required variables without actual values.
For production, env vars are set through the hosting platform (Heroku config vars, AWS Parameter Store, Kubernetes secrets, Docker environment) rather than .env files. Secrets management tools like HashiCorp Vault, AWS Secrets Manager, or Doppler provide rotation, access control, and audit logging for sensitive values.
# .env (in .gitignore - NEVER commit this)
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=super-secret-key-change-in-production
GEMINI_API_KEY=AIza...
PORT=3000
NODE_ENV=development
AWS_BUCKET_NAME=my-uploads-dev
# .env.example (committed to Git - documents required vars)
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=your-secret-here
GEMINI_API_KEY=your-gemini-key
PORT=3000
NODE_ENV=development
AWS_BUCKET_NAME=your-bucket-name
# Load in Node.js
import 'dotenv/config';
// Or:
import dotenv from 'dotenv';
dotenv.config();
// Access variables
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) throw new Error('DATABASE_URL is required');
// Validate all required env vars at startup
const required = ['DATABASE_URL', 'JWT_SECRET', 'GEMINI_API_KEY'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required env var: ${key}`);
}
}
Always validate required env vars at startup. Fail fast with a clear error instead of crashing later with a cryptic message. Use .env.example to document what is needed.
# Multiple .env files for different environments
# .env (default, loaded first)
# .env.local (local overrides, gitignored)
# .env.development (dev-specific)
# .env.production (prod-specific)
# .env.test (test-specific)
# Vite loads env vars with VITE_ prefix
# .env
VITE_API_URL=http://localhost:3000/api
VITE_GOOGLE_CLIENT_ID=your-client-id
# Access in React (Vite)
const apiUrl = import.meta.env.VITE_API_URL;
// Only VITE_ prefixed vars are exposed to client
// This prevents accidentally leaking server secrets
# Shell environment variables
export NODE_ENV=production
export PORT=8080
node dist/index.js
# Inline env vars
DATABASE_URL=postgres://prod@db:5432/app node dist/index.js
# Docker environment variables
docker run -e NODE_ENV=production -e PORT=8080 myapp
# Or from a file:
docker run --env-file .env.production myapp
Client-side frameworks use prefixes (VITE_, NEXT_PUBLIC_) to prevent leaking server secrets. Only prefixed variables are bundled into the client build.
# GitHub Actions secrets
# Set in repo Settings > Secrets > Actions
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
# AWS Systems Manager Parameter Store
aws ssm put-parameter \
--name "/myapp/prod/DATABASE_URL" \
--type SecureString \
--value "postgresql://prod@rds:5432/myapp"
# Retrieve at runtime
aws ssm get-parameter \
--name "/myapp/prod/DATABASE_URL" \
--with-decryption \
--query 'Parameter.Value' --output text
# Kubernetes secrets
kubectl create secret generic app-secrets \
--from-literal=JWT_SECRET=super-secret \
--from-literal=DATABASE_URL=postgres://...
# Reference in pod spec:
# env:
# - name: JWT_SECRET
# valueFrom:
# secretKeyRef:
# name: app-secrets
# key: JWT_SECRET
Never store production secrets in .env files on servers. Use platform-native secrets: GitHub Secrets for CI/CD, AWS SSM/Secrets Manager for AWS, Kubernetes Secrets for K8s.
Environment Variables, dotenv, Secrets Management, 12-Factor App, Configuration