initial commit
This commit is contained in:
692
docs/user-guide/configuration/environment-specific.md
Normal file
692
docs/user-guide/configuration/environment-specific.md
Normal file
@ -0,0 +1,692 @@
|
||||
# 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:
|
||||
|
||||
```env
|
||||
ENVIRONMENT="local" # or "staging" or "production"
|
||||
```
|
||||
|
||||
## Development Environment
|
||||
|
||||
### Local Development Settings
|
||||
|
||||
Create `src/.env.development`:
|
||||
|
||||
```env
|
||||
# ------------- 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
|
||||
|
||||
```python
|
||||
# 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`:
|
||||
|
||||
```yaml
|
||||
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`:
|
||||
|
||||
```env
|
||||
# ------------- 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
|
||||
|
||||
```python
|
||||
# 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`:
|
||||
|
||||
```yaml
|
||||
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`:
|
||||
|
||||
```env
|
||||
# ------------- 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
|
||||
|
||||
```python
|
||||
# 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`:
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
```python
|
||||
@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:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
```bash
|
||||
#!/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
|
||||
|
||||
```python
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
@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!
|
||||
Reference in New Issue
Block a user