Environments
Manage configuration across development, staging, and production.
Environment Strategy
Typical Environment Hierarchy
┌─────────────┐
│ Development │ Local developer machines
└──────┬──────┘
│
┌──────┴──────┐
│ Staging │ Pre-production testing
└──────┬──────┘
│
┌──────┴──────┐
│ Production │ Live customer traffic
└─────────────┘
Environment Purposes
| Environment |
Purpose |
Data |
Access |
| Development |
Local testing |
Fake/seeded |
Developers |
| Staging |
Integration testing |
Anonymized prod |
Team |
| Production |
Live users |
Real |
Restricted |
GitHub Environments
# Repository Settings → Environments
# Staging environment
staging:
deployment_branch_policy:
protected_branches: false
custom_branch_policies: true
- main
- develop
environment_variables:
API_URL: https://staging-api.example.com
secrets:
DATABASE_URL: ***
# Production environment
production:
deployment_branch_policy:
protected_branches: true # Only main
required_reviewers:
- @team-leads
wait_timer: 5 # minutes
Use in Workflows
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_URL: ${{ vars.API_URL }}
run: ./deploy.sh
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }} # Different secret
run: ./deploy.sh
Environment Variables
Configuration Hierarchy
┌─────────────────────────────────────┐
│ Environment Variables │ Highest priority
├─────────────────────────────────────┤
│ .env.local │ Local overrides (gitignored)
├─────────────────────────────────────┤
│ .env.{environment} │ Environment-specific
├─────────────────────────────────────┤
│ .env │ Default values
└─────────────────────────────────────┘
Python Settings
# settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Literal
class Settings(BaseSettings):
environment: Literal["development", "staging", "production"] = "development"
# Database
database_url: str
# API
api_url: str = "http://localhost:8000"
debug: bool = False
# Feature flags
enable_analytics: bool = False
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
@property
def is_production(self) -> bool:
return self.environment == "production"
settings = Settings()
Environment Files
# .env (defaults, committed)
ENVIRONMENT=development
DEBUG=true
ENABLE_ANALYTICS=false
# .env.staging (committed)
ENVIRONMENT=staging
DEBUG=false
API_URL=https://staging-api.example.com
# .env.production (committed, no secrets)
ENVIRONMENT=production
DEBUG=false
API_URL=https://api.example.com
ENABLE_ANALYTICS=true
# .env.local (gitignored, local overrides)
DATABASE_URL=postgresql://localhost/myapp
SECRET_KEY=dev-secret-key
Next.js Environment
# .env.local (development)
NEXT_PUBLIC_API_URL=http://localhost:8000
DATABASE_URL=postgresql://localhost/dev
# .env.production
NEXT_PUBLIC_API_URL=https://api.example.com
// next.config.js
module.exports = {
env: {
NEXT_PUBLIC_ENVIRONMENT: process.env.VERCEL_ENV || 'development',
},
};
Secrets Management
Per-Environment Secrets
# GitHub Actions
jobs:
deploy:
environment: ${{ inputs.environment }}
steps:
- name: Deploy
env:
# Automatically uses environment-specific secret
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.sh
External Secret Stores
# Using AWS Secrets Manager
- name: Get secrets from AWS
uses: aws-actions/aws-secretsmanager-get-secrets@v1
with:
secret-ids: |
${{ inputs.environment }}/database
${{ inputs.environment }}/api-keys
parse-json-secrets: true
- name: Deploy
env:
DATABASE_URL: ${{ env.DATABASE_URL }}
run: ./deploy.sh
Feature Flags by Environment
Configuration-Based
# feature_flags.py
from settings import settings
FEATURE_FLAGS = {
"new_checkout": {
"development": True,
"staging": True,
"production": False,
},
"dark_mode": {
"development": True,
"staging": True,
"production": True,
},
"ai_recommendations": {
"development": True,
"staging": True,
"production": False, # Rolling out gradually
},
}
def is_feature_enabled(feature: str) -> bool:
flags = FEATURE_FLAGS.get(feature, {})
return flags.get(settings.environment, False)
# Usage
if is_feature_enabled("new_checkout"):
return new_checkout_flow()
else:
return legacy_checkout_flow()
External Feature Flag Service
import launchdarkly_client as ld
client = ld.LDClient(settings.launchdarkly_sdk_key)
def is_feature_enabled(feature: str, user: User = None) -> bool:
context = ld.Context.builder(user.id if user else "anonymous").build()
return client.variation(feature, context, False)
Database Per Environment
Separate Databases
# CI/CD secrets
staging:
DATABASE_URL: postgresql://user:pass@staging-db.example.com/app
production:
DATABASE_URL: postgresql://user:pass@prod-db.example.com/app
Database Migrations
migrate:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- name: Run migrations
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: alembic upgrade head
- name: Verify migrations
run: alembic current
URL Configuration
Dynamic URLs
# settings.py
class Settings(BaseSettings):
environment: str = "development"
@property
def api_url(self) -> str:
return {
"development": "http://localhost:8000",
"staging": "https://staging-api.example.com",
"production": "https://api.example.com",
}[self.environment]
@property
def frontend_url(self) -> str:
return {
"development": "http://localhost:3000",
"staging": "https://staging.example.com",
"production": "https://example.com",
}[self.environment]
CORS by Environment
from fastapi.middleware.cors import CORSMiddleware
CORS_ORIGINS = {
"development": ["http://localhost:3000"],
"staging": ["https://staging.example.com"],
"production": ["https://example.com"],
}
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ORIGINS[settings.environment],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
name: Promote to Production
on:
workflow_dispatch:
inputs:
staging_version:
description: 'Staging version to promote'
required: true
jobs:
verify-staging:
runs-on: ubuntu-latest
steps:
- name: Verify staging health
run: |
curl -f https://staging.example.com/health || exit 1
promote:
needs: verify-staging
runs-on: ubuntu-latest
environment: production
steps:
- name: Tag production image
run: |
docker pull myapp:staging-${{ inputs.staging_version }}
docker tag myapp:staging-${{ inputs.staging_version }} myapp:production
docker push myapp:production
- name: Deploy to production
run: ./deploy.sh production
Environment Validation
Startup Checks
# startup.py
import sys
from settings import settings
def validate_environment():
"""Validate required configuration on startup."""
errors = []
# Check required settings
if not settings.database_url:
errors.append("DATABASE_URL is required")
if settings.is_production:
if settings.debug:
errors.append("DEBUG must be False in production")
if "localhost" in settings.database_url:
errors.append("Cannot use localhost database in production")
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
sys.exit(1)
# Run on import
validate_environment()
CI Environment Check
- name: Validate environment config
run: |
python -c "
from settings import settings
assert settings.environment == '${{ inputs.environment }}'
assert settings.database_url.startswith('postgresql://')
print('Environment configuration valid')
"
Best Practices Summary
| Practice |
Benefit |
| Separate environments |
Isolation, safety |
| Environment-specific secrets |
Security |
| Feature flags |
Controlled rollout |
| Validate on startup |
Catch misconfig early |
| No prod data in staging |
Privacy, compliance |
| Require approval for prod |
Change control |
| Document differences |
Team awareness |
| Automate promotion |
Consistency |