Skip to content

Environment-Specific Configuration

Learn how to configure your FastAPI application for different environments (development, staging, production) with appropriate security, performance, and monitoring settings.

Environment Types

The boilerplate supports three environment types:

  • local - Development environment with full debugging
  • staging - Pre-production testing environment
  • production - Production environment with security hardening

Set the environment type with:

ENVIRONMENT="local"  # or "staging" or "production"

Development Environment

Local Development Settings

Create src/.env.development:

# ------------- environment -------------
ENVIRONMENT="local"
DEBUG=true

# ------------- app settings -------------
APP_NAME="MyApp (Development)"
APP_VERSION="0.1.0-dev"

# ------------- database -------------
POSTGRES_USER="dev_user"
POSTGRES_PASSWORD="dev_password"
POSTGRES_SERVER="localhost"
POSTGRES_PORT=5432
POSTGRES_DB="myapp_dev"

# ------------- crypt -------------
SECRET_KEY="dev-secret-key-not-for-production-use"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=60  # Longer for development
REFRESH_TOKEN_EXPIRE_DAYS=30     # Longer for development

# ------------- redis -------------
REDIS_CACHE_HOST="localhost"
REDIS_CACHE_PORT=6379
REDIS_QUEUE_HOST="localhost"
REDIS_QUEUE_PORT=6379
REDIS_RATE_LIMIT_HOST="localhost"
REDIS_RATE_LIMIT_PORT=6379

# ------------- caching -------------
CLIENT_CACHE_MAX_AGE=0  # Disable caching for development

# ------------- rate limiting -------------
DEFAULT_RATE_LIMIT_LIMIT=1000   # Higher limits for development
DEFAULT_RATE_LIMIT_PERIOD=3600

# ------------- admin -------------
ADMIN_NAME="Dev Admin"
ADMIN_EMAIL="admin@localhost"
ADMIN_USERNAME="admin"
ADMIN_PASSWORD="admin123"

# ------------- tier -------------
TIER_NAME="dev_tier"

# ------------- logging -------------
DATABASE_ECHO=true  # Log all SQL queries

Development Features

# Development-specific features
if settings.ENVIRONMENT == "local":
    # Enable detailed error pages
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],  # Allow all origins in development
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # Enable API documentation
    app.openapi_url = "/openapi.json"
    app.docs_url = "/docs"
    app.redoc_url = "/redoc"

Docker Development Override

docker-compose.override.yml:

version: '3.8'

services:
  web:
    environment:
      - ENVIRONMENT=local
      - DEBUG=true
      - DATABASE_ECHO=true
    volumes:
      - ./src:/code/src:cached
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    ports:
      - "8000:8000"

  db:
    environment:
      - POSTGRES_DB=myapp_dev
    ports:
      - "5432:5432"

  redis:
    ports:
      - "6379:6379"

  # Development tools
  adminer:
    image: adminer
    ports:
      - "8080:8080"
    depends_on:
      - db

Staging Environment

Staging Settings

Create src/.env.staging:

# ------------- environment -------------
ENVIRONMENT="staging"
DEBUG=false

# ------------- app settings -------------
APP_NAME="MyApp (Staging)"
APP_VERSION="0.1.0-staging"

# ------------- database -------------
POSTGRES_USER="staging_user"
POSTGRES_PASSWORD="complex_staging_password_123!"
POSTGRES_SERVER="staging-db.example.com"
POSTGRES_PORT=5432
POSTGRES_DB="myapp_staging"

# ------------- crypt -------------
SECRET_KEY="staging-secret-key-different-from-production"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=7

# ------------- redis -------------
REDIS_CACHE_HOST="staging-redis.example.com"
REDIS_CACHE_PORT=6379
REDIS_QUEUE_HOST="staging-redis.example.com"
REDIS_QUEUE_PORT=6379
REDIS_RATE_LIMIT_HOST="staging-redis.example.com"
REDIS_RATE_LIMIT_PORT=6379

# ------------- caching -------------
CLIENT_CACHE_MAX_AGE=300  # 5 minutes

# ------------- rate limiting -------------
DEFAULT_RATE_LIMIT_LIMIT=100
DEFAULT_RATE_LIMIT_PERIOD=3600

# ------------- admin -------------
ADMIN_NAME="Staging Admin"
ADMIN_EMAIL="admin@staging.example.com"
ADMIN_USERNAME="staging_admin"
ADMIN_PASSWORD="secure_staging_password_456!"

# ------------- tier -------------
TIER_NAME="staging_tier"

# ------------- logging -------------
DATABASE_ECHO=false

Staging Features

# Staging-specific features
if settings.ENVIRONMENT == "staging":
    # Restricted CORS
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["https://staging.example.com"],
        allow_credentials=True,
        allow_methods=["GET", "POST", "PUT", "DELETE"],
        allow_headers=["*"],
    )

    # API docs available to superusers only
    @app.get("/docs", include_in_schema=False)
    async def custom_swagger_ui(current_user: User = Depends(get_current_superuser)):
        return get_swagger_ui_html(openapi_url="/openapi.json")

Docker Staging Configuration

docker-compose.staging.yml:

version: '3.8'

services:
  web:
    environment:
      - ENVIRONMENT=staging
      - DEBUG=false
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 1G
        reservations:
          memory: 512M
    restart: always

  db:
    environment:
      - POSTGRES_DB=myapp_staging
    volumes:
      - postgres_staging_data:/var/lib/postgresql/data
    restart: always

  redis:
    restart: always

  worker:
    deploy:
      replicas: 2
    restart: always

volumes:
  postgres_staging_data:

Production Environment

Production Settings

Create src/.env.production:

# ------------- environment -------------
ENVIRONMENT="production"
DEBUG=false

# ------------- app settings -------------
APP_NAME="MyApp"
APP_VERSION="1.0.0"
CONTACT_NAME="Support Team"
CONTACT_EMAIL="support@example.com"

# ------------- database -------------
POSTGRES_USER="prod_user"
POSTGRES_PASSWORD="ultra_secure_production_password_789!"
POSTGRES_SERVER="prod-db.example.com"
POSTGRES_PORT=5433  # Custom port for security
POSTGRES_DB="myapp_production"

# ------------- crypt -------------
SECRET_KEY="ultra-secure-production-key-generated-with-openssl-rand-hex-32"
ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=15  # Shorter for security
REFRESH_TOKEN_EXPIRE_DAYS=3     # Shorter for security

# ------------- redis -------------
REDIS_CACHE_HOST="prod-redis.example.com"
REDIS_CACHE_PORT=6380  # Custom port for security
REDIS_QUEUE_HOST="prod-redis.example.com"
REDIS_QUEUE_PORT=6380
REDIS_RATE_LIMIT_HOST="prod-redis.example.com"
REDIS_RATE_LIMIT_PORT=6380

# ------------- caching -------------
CLIENT_CACHE_MAX_AGE=3600  # 1 hour

# ------------- rate limiting -------------
DEFAULT_RATE_LIMIT_LIMIT=100
DEFAULT_RATE_LIMIT_PERIOD=3600

# ------------- admin -------------
ADMIN_NAME="System Administrator"
ADMIN_EMAIL="admin@example.com"
ADMIN_USERNAME="sysadmin"
ADMIN_PASSWORD="extremely_secure_admin_password_with_symbols_#$%!"

# ------------- tier -------------
TIER_NAME="production_tier"

# ------------- logging -------------
DATABASE_ECHO=false

Production Security Features

# Production-specific features
if settings.ENVIRONMENT == "production":
    # Strict CORS
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["https://example.com", "https://www.example.com"],
        allow_credentials=True,
        allow_methods=["GET", "POST", "PUT", "DELETE"],
        allow_headers=["Authorization", "Content-Type"],
    )

    # Disable API documentation
    app.openapi_url = None
    app.docs_url = None
    app.redoc_url = None

    # Add security headers
    @app.middleware("http")
    async def add_security_headers(request: Request, call_next):
        response = await call_next(request)
        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["X-Frame-Options"] = "DENY"
        response.headers["X-XSS-Protection"] = "1; mode=block"
        response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
        return response

Docker Production Configuration

docker-compose.prod.yml:

version: '3.8'

services:
  web:
    environment:
      - ENVIRONMENT=production
      - DEBUG=false
    deploy:
      replicas: 3
      resources:
        limits:
          memory: 2G
          cpus: '1'
        reservations:
          memory: 1G
          cpus: '0.5'
    restart: always
    ports: []  # No direct exposure

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/ssl:/etc/nginx/ssl
      - ./nginx/htpasswd:/etc/nginx/htpasswd
    depends_on:
      - web
    restart: always

  db:
    environment:
      - POSTGRES_DB=myapp_production
    volumes:
      - postgres_prod_data:/var/lib/postgresql/data
    ports: []  # No external access
    deploy:
      resources:
        limits:
          memory: 4G
        reservations:
          memory: 2G
    restart: always

  redis:
    volumes:
      - redis_prod_data:/data
    ports: []  # No external access
    deploy:
      resources:
        limits:
          memory: 1G
        reservations:
          memory: 512M
    restart: always

  worker:
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 1G
        reservations:
          memory: 512M
    restart: always

volumes:
  postgres_prod_data:
  redis_prod_data:

Environment Detection

Runtime Environment Checks

# src/app/core/config.py
class Settings(BaseSettings):
    @computed_field
    @property
    def IS_DEVELOPMENT(self) -> bool:
        return self.ENVIRONMENT == "local"

    @computed_field
    @property
    def IS_PRODUCTION(self) -> bool:
        return self.ENVIRONMENT == "production"

    @computed_field
    @property
    def IS_STAGING(self) -> bool:
        return self.ENVIRONMENT == "staging"

# Use in application
if settings.IS_DEVELOPMENT:
    # Development-only code
    pass

if settings.IS_PRODUCTION:
    # Production-only code
    pass

Environment-Specific Validation

@model_validator(mode="after")
def validate_environment_config(self) -> "Settings":
    if self.ENVIRONMENT == "production":
        # Production validation
        if self.DEBUG:
            raise ValueError("DEBUG must be False in production")
        if len(self.SECRET_KEY) < 32:
            raise ValueError("SECRET_KEY must be at least 32 characters in production")
        if "dev" in self.SECRET_KEY.lower():
            raise ValueError("Production SECRET_KEY cannot contain 'dev'")

    if self.ENVIRONMENT == "local":
        # Development warnings
        if not self.DEBUG:
            logger.warning("DEBUG is False in development environment")

    return self

Configuration Management

Environment File Templates

Create template files for each environment:

# Create environment templates
cp src/.env.example src/.env.development
cp src/.env.example src/.env.staging
cp src/.env.example src/.env.production

# Use environment-specific files
ln -sf .env.development src/.env  # For development
ln -sf .env.staging src/.env      # For staging
ln -sf .env.production src/.env   # For production

Configuration Validation

# src/scripts/validate_config.py
import asyncio
from src.app.core.config import settings
from src.app.core.db.database import async_get_db

async def validate_configuration():
    """Validate configuration for current environment."""
    print(f"Validating configuration for {settings.ENVIRONMENT} environment...")

    # Basic settings validation
    assert settings.APP_NAME, "APP_NAME is required"
    assert settings.SECRET_KEY, "SECRET_KEY is required"
    assert len(settings.SECRET_KEY) >= 32, "SECRET_KEY must be at least 32 characters"

    # Environment-specific validation
    if settings.ENVIRONMENT == "production":
        assert not settings.DEBUG, "DEBUG must be False in production"
        assert "dev" not in settings.SECRET_KEY.lower(), "Production SECRET_KEY invalid"
        assert settings.POSTGRES_PORT != 5432, "Use custom PostgreSQL port in production"

    # Test database connection
    try:
        db = await anext(async_get_db())
        print("✓ Database connection successful")
        await db.close()
    except Exception as e:
        print(f"✗ Database connection failed: {e}")
        return False

    print("✓ Configuration validation passed")
    return True

if __name__ == "__main__":
    asyncio.run(validate_configuration())

Environment Switching

#!/bin/bash
# scripts/switch_env.sh

ENV=$1

if [ -z "$ENV" ]; then
    echo "Usage: $0 <development|staging|production>"
    exit 1
fi

case $ENV in
    development)
        ln -sf .env.development src/.env
        echo "Switched to development environment"
        ;;
    staging)
        ln -sf .env.staging src/.env
        echo "Switched to staging environment"
        ;;
    production)
        ln -sf .env.production src/.env
        echo "Switched to production environment"
        echo "WARNING: Make sure to review all settings before deployment!"
        ;;
    *)
        echo "Invalid environment: $ENV"
        echo "Valid options: development, staging, production"
        exit 1
        ;;
esac

# Validate configuration
python -c "from src.app.core.config import settings; print(f'Current environment: {settings.ENVIRONMENT}')"

Security Best Practices

Environment-Specific Security

# Different security levels per environment
SECURITY_CONFIGS = {
    "local": {
        "token_expire_minutes": 60,
        "enable_cors_origins": ["*"],
        "enable_docs": True,
        "log_level": "DEBUG",
    },
    "staging": {
        "token_expire_minutes": 30,
        "enable_cors_origins": ["https://staging.example.com"],
        "enable_docs": True,  # For testing
        "log_level": "INFO",
    },
    "production": {
        "token_expire_minutes": 15,
        "enable_cors_origins": ["https://example.com"],
        "enable_docs": False,
        "log_level": "WARNING",
    }
}

config = SECURITY_CONFIGS[settings.ENVIRONMENT]

Secrets Management

# Use secrets management in production
# Instead of plain text environment variables
POSTGRES_PASSWORD_FILE="/run/secrets/postgres_password"
SECRET_KEY_FILE="/run/secrets/jwt_secret"

# Docker secrets
services:
  web:
    secrets:
      - postgres_password
      - jwt_secret
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
      - SECRET_KEY_FILE=/run/secrets/jwt_secret

secrets:
  postgres_password:
    external: true
  jwt_secret:
    external: true

Monitoring and Logging

Environment-Specific Logging

LOGGING_CONFIG = {
    "local": {
        "level": "DEBUG",
        "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        "handlers": ["console"],
    },
    "staging": {
        "level": "INFO", 
        "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
        "handlers": ["console", "file"],
    },
    "production": {
        "level": "WARNING",
        "format": "%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s",
        "handlers": ["file", "syslog"],
    }
}

Health Checks by Environment

@app.get("/health")
async def health_check():
    health_info = {
        "status": "healthy",
        "environment": settings.ENVIRONMENT,
        "version": settings.APP_VERSION,
    }

    # Add detailed info in non-production
    if not settings.IS_PRODUCTION:
        health_info.update({
            "database": await check_database_health(),
            "redis": await check_redis_health(),
            "worker_queue": await check_worker_health(),
        })

    return health_info

Best Practices

Security

  • Use different secret keys for each environment
  • Disable debug mode in staging and production
  • Use custom ports in production
  • Implement proper CORS policies
  • Remove API documentation in production

Performance

  • Configure appropriate resource limits per environment
  • Use caching in staging and production
  • Set shorter token expiration in production
  • Use connection pooling in production

Configuration

  • Keep environment files in version control (except production)
  • Use validation to prevent misconfiguration
  • Document all environment-specific settings
  • Test configuration changes in staging first

Monitoring

  • Use appropriate log levels per environment
  • Monitor different metrics in each environment
  • Set up alerts for production only
  • Use health checks for all environments

Environment-specific configuration ensures your application runs securely and efficiently in each deployment stage. Start with development settings and progressively harden for production!