DEV Community

EME GUG
EME GUG

Posted on

Environment variables done right

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
Enter fullscreen mode Exit fullscreen mode
// Load with dotenv (Node.js)
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

Common Mistakes

  1. Default values for secrets: API_KEY || 'default' — fails silently in production
  2. Logging env vars: console.log(config) — prints secrets to log files
  3. Different names across environments: DB_URL vs DATABASE_URL vs DB_CONNECTION_STRING
  4. 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
Enter fullscreen mode Exit fullscreen mode

How do you manage secrets in production? Always curious about different setups.

Top comments (1)

Collapse
 
theoephraim profile image
Theo Ephraim

just use varlock!