I've seen API keys committed to Git, .env files with 50+ variables, and production secrets stored in plain text files. Here's how to manage environment variables properly.
The Basics
# .env file (development only)
DATABASE_URL=postgresql://localhost:5432/mydb
REDIS_URL=redis://localhost:6379
API_KEY=dev_key_123
PORT=3000
// Load with dotenv (Node.js)
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
Rule #1: .env goes in .gitignore. Always.
Structure Your Variables
# .env.example (committed to Git — no real values)
DATABASE_URL=
REDIS_URL=
API_KEY=
PORT=3000
# .env.development
DATABASE_URL=postgresql://localhost:5432/mydb_dev
# .env.test
DATABASE_URL=postgresql://localhost:5432/mydb_test
# .env.production (never in Git)
DATABASE_URL=postgresql://prod-host:5432/mydb_prod
Validation at Startup
Don't wait for the first database call to discover DATABASE_URL is missing.
// config.js
const required = ['DATABASE_URL', 'REDIS_URL', 'API_KEY'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
console.error(`Missing env vars: ${missing.join(', ')}`);
process.exit(1);
}
module.exports = {
db: process.env.DATABASE_URL,
redis: process.env.REDIS_URL,
apiKey: process.env.API_KEY,
port: parseInt(process.env.PORT || '3000'),
isDev: process.env.NODE_ENV !== 'production',
};
Secrets Management (Production)
Option 1: Platform secrets
# GitHub Actions
env:
API_KEY: ${{ secrets.API_KEY }}
# Docker Compose
services:
app:
environment:
- API_KEY=${API_KEY} # From host environment
Option 2: Vault/AWS Secrets Manager
import boto3
def get_secret(name):
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId=name)
return json.loads(response['SecretString'])
secrets = get_secret('myapp/production')
db_url = secrets['DATABASE_URL']
Option 3: Encrypted .env
# Encrypt
gpg --symmetric --cipher-algo AES256 .env.production
# Creates .env.production.gpg (safe to commit)
# Decrypt
gpg --decrypt .env.production.gpg > .env.production
Common Mistakes
-
Default values for secrets:
API_KEY || 'default'— fails silently in production -
Logging env vars:
console.log(config)— prints secrets to log files -
Different names across environments:
DB_URLvsDATABASE_URLvsDB_CONNECTION_STRING - Putting non-secrets in env vars: App name, feature flags → use config files instead
Docker
# Bad: bakes secrets into image layer
ENV API_KEY=secret123
# Good: pass at runtime
# docker run -e API_KEY=secret123 myapp
# or: docker run --env-file .env myapp
How do you manage secrets in production? Always curious about different setups.
Top comments (1)
just use varlock!