Environment Variable Management: Security and Best Practices
Environment variables separate configuration from code, keeping secrets out of version control. Mismanaging them leads to leaked API keys, broken deployments, and configuration drift between environments.
Key Takeaways
- Hardcoding configuration values — database URLs, API keys, feature flags — into source code creates three problems: secrets end up in version control (and Git history forever), the same code can't run in different environments without modification, and configuration changes require code deployments.
- `.env` files store key-value pairs loaded at application startup:
- Maintain separate configurations for each environment:
- For small teams, 1Password or similar tools can store and sync `.env` files.
- Using different variable names across environments (DB_HOST vs DATABASE_HOST) causes confusion.
Why Environment Variables Exist
Hardcoding configuration values — database URLs, API keys, feature flags — into source code creates three problems: secrets end up in version control (and Git history forever), the same code can't run in different environments without modification, and configuration changes require code deployments.
Environment variables solve all three by storing configuration outside the codebase, injected at runtime.
The .env File Pattern
Structure
.env files store key-value pairs loaded at application startup:
DATABASE_URL=postgres://user:pass@host:5432/dbname
DJANGO_SECRET_KEY=a-long-random-string-here
STRIPE_API_KEY=sk_live_...
DEBUG=false
ALLOWED_HOSTS=example.com,www.example.com
Critical Rule: Never Commit .env Files
Add .env and .env.prod to .gitignore immediately when starting a project. If a secret is committed even once, it exists in Git history forever — even after deleting the file. Leaked secrets should be rotated immediately, not just removed from the repository.
.env.example
Maintain a .env.example file (committed to Git) that lists all required variables with placeholder values. This documents what configuration a new developer needs without exposing actual secrets:
DATABASE_URL=postgres://user:password@localhost:5432/myapp
DJANGO_SECRET_KEY=change-me-to-a-random-string
STRIPE_API_KEY=sk_test_...
Environment Separation
Per-Environment Configuration
Maintain separate configurations for each environment:
| Variable | Development | Staging | Production |
|---|---|---|---|
| DEBUG | true | false | false |
| DATABASE_URL | localhost | staging-rds | production-rds |
| LOG_LEVEL | DEBUG | INFO | WARNING |
| API keys | Test keys | Test keys | Live keys |
Never use production secrets in development. Most third-party services provide separate test/sandbox keys for this reason.
Secret Management at Scale
Password Managers
For small teams, 1Password or similar tools can store and sync .env files. A sync-secrets command pulls the latest values from the password manager and writes them to the local .env file.
Cloud Secret Managers
For larger deployments, use AWS Secrets Manager, Google Secret Manager, or HashiCorp Vault. These provide access control, audit logging, automatic rotation, and versioning. Applications fetch secrets at startup rather than reading from files.
Common Mistakes
Using different variable names across environments (DB_HOST vs DATABASE_HOST) causes confusion. Default values in code that silently override missing environment variables can mask configuration errors. Always fail loudly if a required environment variable is missing.