Infrastructure
Environment Management
Development, staging, and production are different worlds. Development runs on your laptop, staging mirrors production, production is live. Managing configuration and secrets across these environments is critical—do it wrong and you'll deploy with the wrong database URL or expose production keys.
The Three Environments
Development runs on your machine. You can break things, run things slowly, use test data. Nothing here matters except getting code working.
Staging is a copy of production. Same servers, same database configuration, same infrastructure. Why? To catch environment-specific bugs before production. Many bugs only appear on the production infrastructure, not on your laptop.
Production is live. Real users, real data, real money. A broken production deployment costs money and damages your reputation.
Many teams skip staging and go straight from development to production. This is a mistake that costs them in firefighting and outages.
Environment Parity
Environment parity means development, staging, and production are as similar as possible. Same operating system, same runtime versions, same database version, same libraries.
Why does this matter? Because of bugs that only appear in production. "Works on my machine" often happens because the developer's machine is different from production. Different Node version, different Python version, different database version.
Environment parity reduces these bugs dramatically. If your laptop runs the same configuration as production, bugs become obvious before you deploy.
Docker helps with environment parity—the same container runs on your laptop and in production. Infrastructure-as-code tools (Terraform) ensure servers are configured identically. Both reduce production surprises.
Environment Variables
Configuration that changes between environments is stored in environment variables:
DATABASE_URL=postgres://localhost/myapp_dev
API_SECRET=dev-secret-123
LOG_LEVEL=debug
PORT=3000
In development, DATABASE_URL points to your local database. In staging, it points to staging database. In production, it points to production database. The code is identical; only the environment variables change.
.env files store these locally:
# .env file in your project root
DATABASE_URL=postgres://localhost/myapp_dev
API_SECRET=dev-secret
NODE_ENV=development
Add .env to .gitignore so it's not committed:
# .gitignore
.env
.env.local
Create a .env.example with dummy values to show what variables are needed:
# .env.example
DATABASE_URL=postgres://user:password@localhost/myapp_dev
API_SECRET=your-secret-here
NODE_ENV=development
New developers copy .env.example to .env and fill in their values. This ensures everyone knows what configuration is needed.
Secrets Management
For production, don't use .env files. Use a secrets manager. Options:
AWS Secrets Manager: Store secrets in AWS, retrieve them at runtime. Integrates with IAM for access control. Expensive but comprehensive.
HashiCorp Vault: Open-source secrets management. Can be self-hosted or used as a service. Powerful but complex to operate.
Doppler: Secrets management as a service. Simple interface, CLI integration, good for small teams. Pay-per-secret.
Environment variables in your hosting platform: Vercel, Railway, Heroku all let you set environment variables in their UI. Secrets are encrypted at rest. Simple for small projects.
For most projects, your hosting platform's secrets management is sufficient. For complex organizations, dedicated secrets managers are necessary.
Infrastructure as Code (IaC)
Your infrastructure shouldn't be a mystery. "The database is on some AWS server somewhere" is bad. You should define your infrastructure in code, version it, and deploy it.
Terraform is the standard tool. Define infrastructure in HCL:
resource "aws_rds_instance" "db" {
allocated_storage = 100
storage_type = "gp2"
engine = "postgres"
engine_version = "15.1"
instance_class = "db.t3.micro"
}
This creates an RDS database. You can version control it, review changes, and deploy the same infrastructure to multiple environments.
Pulumi is similar but uses programming languages (Python, Go, JavaScript) instead of domain-specific languages. If you prefer code over configuration, Pulumi is good.
IaC has huge benefits: reproducible infrastructure, version history, code review before deploying infrastructure changes. Infrastructure changes are reviewed like code changes.
Feature Flags for Environment Control
Not all configuration fits in environment variables. Feature flags let you enable/disable features at runtime without redeploying:
if (featureFlags.darkMode) {
// Show dark mode UI
}
This feature is disabled in production, enabled in staging. You can flip flags without redeploying. Use cases: beta features, A/B testing, emergency disabling of broken features.
Tools like LaunchDarkly, Statsig, or Split.io manage feature flags. For simple cases, a database table works fine.
Database Configuration Across Environments
Should you share a database across environments or use separate databases?
Separate databases: Development has its own database, staging has its own, production has its own. This is safer. A developer mistake in development doesn't affect production data. Separate databases are harder to keep synchronized but safer.
Shared database with different schemas: One database with separate schemas for dev, staging, prod. Slightly cheaper but mixing databases is risky.
Production data in staging: Copy production database to staging daily. Staging tests against real data. This finds bugs but requires compliance review—real data in staging.
Most teams use separate databases. It's safer and cleaner.
Documenting Environment Configuration
Document what configuration your application needs. What environment variables? What values in each environment? What secrets?
Create a README or wiki page:
# Environment Configuration
## Required Variables
- DATABASE_URL: PostgreSQL connection string
- API_KEY: Third-party API key (get from Doppler)
- STRIPE_SECRET: Stripe API key (from secrets manager)
## Development
DATABASE_URL=postgres://localhost/myapp
API_KEY=dev-key
## Staging DATABASE_URL=(see Doppler)
API_KEY=(see Doppler)
This prevents the "I need the database URL for staging" Slack message at 3am.
Common Environment Mistakes
Testing against production. A developer runs a database cleanup script. They meant to target development but accidentally targeted production. All data deleted. Always use different database URLs and make it hard to accidentally pick the wrong one.
Hardcoded configuration. "Database host is 'db.production.internal'." Hardcoded in code. You can't run this code in a different environment. Make everything configurable.
Inconsistent environment variables. Development calls it `DB_HOST`, staging calls it `DATABASE_HOST`, production calls it `database_hostname`. Code breaks in each environment. Use consistent names.
No documentation. "Only Alice knows how to deploy to staging." Alice leaves, team is stuck. Document everything. Make it available to the team.
Environment-Specific Gotchas
Different timeouts: Your laptop has fast disk I/O. Production database might be remote. Code that's fine on your laptop times out in production.
Different CPU/memory: Your laptop might have 16GB RAM. Production server has 2GB. Code that works locally crashes in production.
Different timezone handling: Your database is UTC, your laptop is in America/New_York. Date/time bugs appear only in production.
These are arguments for environment parity and staging testing. Catch these before production.
Development Environment Setup
Make it easy for new developers to set up their environment. A good onboarding:
- Clone the repository
- Copy .env.example to .env
- Run `docker compose up`
- Run `npm install && npm run migrations`
- Visit localhost:3000
If setup takes hours, you'll lose developers. Make it 5 minutes. Docker and docker-compose are perfect for this.
The Reality
Environment management is not glamorous. It's not fun to think about. But it's absolutely critical. Most serious production incidents are environment-related: wrong database, missing secret, configuration drift between environments.
Invest in getting it right early. Use environment variables, use secrets managers, use infrastructure as code. Document everything. Set up staging that mirrors production. This saves you from disasters later.