How We Manage Environments
Environment configuration, secrets management, and deployment strategies
How We Manage Environments
Your application runs in different contexts: your laptop, a staging server, production. Each has different needs. This guide covers how to manage these differences without drowning in configuration complexity.
The Core Principle
"Same code, different configuration."
Your application code shouldn't change between environments. Only configuration changes: database connections, API keys, feature flags, URLs.
Environment Types
┌─────────────────────────────────────────────────────────────┐
│ DEVELOPMENT │
│ Local machine, hot reload, debug tools │
│ Database: Local / Docker │
│ API Keys: Test/sandbox credentials │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ STAGING │
│ Production-like, for testing │
│ Database: Separate from production │
│ API Keys: Test/sandbox credentials │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PRODUCTION │
│ Real users, real data │
│ Database: Production │
│ API Keys: Live credentials │
└─────────────────────────────────────────────────────────────┘
Principles
1. Never Commit Secrets
Secrets don't belong in version control. Ever.
The mistake: "I'll just commit the API key for now and change it later." You forget. The key is in git history forever.
The principle: Secrets go in environment variables, set outside your codebase. Even on your local machine.
What counts as a secret:
- API keys and tokens
- Database passwords
- Encryption keys
- OAuth client secrets
- Webhook signing secrets
Not secrets (okay to commit):
- Configuration templates with placeholder values
- Non-sensitive defaults
- Public API URLs
2. Document, Don't Assume
New team members shouldn't have to guess what variables are needed.
The mistake: The app fails on startup with a cryptic error because a required variable is missing. Nobody documented it.
The principle: Maintain a template file (.env.example) that lists every variable, with descriptions and example values. Keep it updated.
What to include:
- Variable name
- Description of what it does
- Example value (not a real secret)
- Whether it's required or optional
- Where to get a valid value
3. Validate Early
Catch configuration problems at startup, not at 3 AM when that code path runs.
The mistake: A typo in a variable name goes unnoticed for weeks. Then a critical feature fails because DATABASE_URL was misspelled.
The principle: Check all required configuration when the application starts. Fail fast with clear error messages.
What to validate:
- Required variables exist
- Values are the right type (URLs are URLs, numbers are numbers)
- Connections work (can you reach the database?)
4. Separate Concerns
Different types of configuration have different needs.
The categories:
| Type | Example | Changes | Security |
|---|---|---|---|
| Secrets | API keys | Rarely | High |
| Environment | Database URL | Per environment | Medium |
| Feature flags | Enable new UI | Frequently | Low |
| Constants | Retry count | Rarely | Low |
The principle: Don't treat all configuration the same. Secrets need protection. Feature flags need easy changes. Constants can be in code.
5. Parity Matters
Development should be as close to production as possible.
The mistake: Everything works locally with SQLite. Production uses PostgreSQL. You discover the incompatibility during deployment.
The principle: Use the same services locally that you use in production. If production uses PostgreSQL, run PostgreSQL locally (Docker makes this easy).
Why it matters: Every difference between environments is a potential bug that only appears in production.
Decision Framework
"Should this be an environment variable?"
Yes, make it a variable when:
- It changes between environments
- It's a secret
- You want to change it without code changes
- It's a connection string or URL
No, keep it in code when:
- It never changes across environments
- It's not sensitive
- It's a constant that defines behavior (retry counts, timeouts)
The test: Would you want to change this without deploying new code? If yes, make it a variable.
"Which file should this go in?"
.env.example (committed):
- Template listing all variables
- Placeholder or example values
- Documentation
.env.local (not committed):
- Your local overrides
- Your personal API keys
- Development-specific settings
.env (sometimes committed):
- Non-sensitive defaults
- Shared development configuration
Platform environment (for production):
- All secrets
- Production URLs
- Production-specific settings
"How should I organize environment variables?"
Group by purpose:
- Database configuration
- Authentication
- External APIs
- Feature flags
- URLs
Use consistent naming:
- Prefix related variables:
DATABASE_URL,DATABASE_POOL_SIZE - Use SCREAMING_SNAKE_CASE
- Be descriptive:
STRIPE_SECRET_KEYnotSK
Add comments in your template:
- Explain non-obvious variables
- Note where to get values
- Mark required vs optional
"What should I do when a variable is missing?"
For required variables:
- Fail at startup with a clear error
- Name the variable that's missing
- Suggest how to fix it
For optional variables:
- Use sensible defaults
- Document the default behavior
- Log when falling back to defaults
Never:
- Silently use an empty string
- Let the app start and fail later
- Swallow the error
"How do I handle different databases per environment?"
Common pattern:
| Environment | Database | Set via |
|---|---|---|
| Development | Local SQLite or Docker PostgreSQL | .env.local or default |
| Staging | Cloud PostgreSQL (separate) | Platform environment |
| Production | Cloud PostgreSQL (production) | Platform environment |
The principle: If DATABASE_URL is empty, use a local fallback. If set, use the provided connection.
Why separate staging and production: Staging is for testing. You don't want test data in your production database, and you don't want to accidentally break production while testing.
Common Mistakes
Hardcoded Configuration
Values that should be configurable are embedded in code.
Signs: You need to change code and redeploy to point to a different API. Different branches exist just for different environments.
The fix: Extract configurable values to environment variables. Make the code environment-agnostic.
Missing Template
No documentation of what variables are needed.
Signs: Onboarding a new developer takes hours of "what variables do I need?" New deployments fail with missing config.
The fix: Create and maintain .env.example. Update it when you add or remove variables.
Secrets in Git
Sensitive values committed to the repository.
Signs: .env files in git history. API keys visible in the codebase. Secret rotation is a nightmare.
The fix: Add .env.local and similar files to .gitignore. If secrets were committed, rotate them immediately. Consider using tools that scan for leaked secrets.
Environment-Specific Code
Different code paths based on environment name.
Signs: if (process.env.NODE_ENV === 'production') scattered everywhere. Bugs that only appear in staging. Features that work differently in prod.
The fix: Configure behavior via environment variables, not environment names. If production needs different behavior, that behavior is a configuration option.
Missing Validation
App starts with invalid configuration.
Signs: Runtime errors from missing variables. Features silently fail. Hard-to-debug production issues.
The fix: Validate all configuration at startup. Fail fast with clear messages.
Local and Production Drift
Your local setup is nothing like production.
Signs: "Works on my machine." Bugs that only appear in production. Surprised by behavior differences.
The fix: Use the same database, same services, same setup locally. Docker makes this manageable.
How to Evaluate Your Environment Setup
Your setup is working if:
- New developers can get running with clear instructions
- Secrets are not in version control
- Missing configuration fails at startup with helpful errors
- You can deploy to any environment without code changes
- Local development resembles production
Your setup needs work if:
- Onboarding requires tribal knowledge
- Developers share API keys via chat
- Environment issues are discovered in production
- Different environments require different code
- You're not sure what's configured in production
Environment Setup Checklist
For New Projects
- Create
.env.examplewith all variables documented - Add
.env.localand.env*.localto.gitignore - Set up validation at startup
- Document setup in README
- Configure production secrets in your deployment platform
For New Team Members
- Copy
.env.exampleto.env.local - Fill in required values (get from team lead or password manager)
- Install local dependencies (database, etc.)
- Run and verify the app starts
For Adding New Variables
- Add to
.env.examplewith description - Add to your
.env.local - Add validation if required
- Add to production environment
- Deploy and verify
The Secret Management Hierarchy
From least secure to most secure:
- Plain text files (never for secrets in production)
- Environment variables (minimum acceptable for secrets)
- Encrypted at rest (platform secrets management)
- Secrets management service (Vault, AWS Secrets Manager)
- Hardware security modules (for the most sensitive keys)
The principle: Match the security level to the sensitivity. Most apps are fine with platform environment variables. Financial or health data may need more.
Quick Reference
| Principle | Implementation |
|---|---|
| Never commit secrets | .gitignore, platform env |
| Document everything | .env.example |
| Validate at startup | Fail fast on missing config |
| Same code, different config | Environment variables |
| Development = Production | Docker, same services |