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!