Production Deployment¶
Complete production deployment guide for FraiseQL: Docker, Kubernetes, environment management, health checks, scaling strategies, and rollback procedures.
Overview¶
Deploy FraiseQL applications to production with confidence using battle-tested patterns for Docker containers, Kubernetes orchestration, and zero-downtime deployments.
Deployment Targets: - Docker (standalone or Compose) - Kubernetes (with Helm charts) - Cloud platforms (GCP, AWS, Azure) - Edge/CDN deployments
Docker Deployment¶
Production Dockerfile¶
Multi-stage build optimized for security and size:
# Stage 1: Builder
FROM python:3.13-slim AS builder
# Install build dependencies
RUN apt-get update && apt-get install -y \
gcc \
g++ \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
# Copy dependency files
COPY pyproject.toml README.md ./
COPY src ./src
# Build wheel
RUN pip install --no-cache-dir build && \
python -m build --wheel
# Stage 2: Runtime
FROM python:3.13-slim
# Runtime dependencies only
RUN apt-get update && apt-get install -y \
libpq5 \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -r fraiseql && useradd -r -g fraiseql fraiseql
WORKDIR /app
# Copy wheel from builder
COPY --from=builder /build/dist/*.whl /tmp/
# Install FraiseQL + production dependencies
RUN pip install --no-cache-dir \
/tmp/*.whl \
uvicorn[standard]==0.24.0 \
gunicorn==21.2.0 \
prometheus-client==0.19.0 \
sentry-sdk[fastapi]==1.38.0 \
&& rm -rf /tmp/*.whl
# Copy application code
COPY app /app
# Set permissions
RUN chown -R fraiseql:fraiseql /app
# Switch to non-root user
USER fraiseql
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
FRAISEQL_ENVIRONMENT=production
# Run with Gunicorn
CMD ["gunicorn", "app:app", \
"-w", "4", \
"-k", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"--log-level", "info"]
Docker Compose Production¶
version: '3.8'
services:
fraiseql:
build:
context: .
dockerfile: Dockerfile
image: fraiseql:${VERSION:-latest}
container_name: fraiseql-app
restart: unless-stopped
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:${DB_PASSWORD}@postgres:5432/fraiseql
- ENVIRONMENT=production
- LOG_LEVEL=INFO
- SENTRY_DSN=${SENTRY_DSN}
env_file:
- .env.production
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
deploy:
resources:
limits:
cpus: '2'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
networks:
- fraiseql-network
postgres:
image: postgres:16-alpine
container_name: fraiseql-postgres
restart: unless-stopped
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=fraiseql
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user"]
interval: 10s
timeout: 5s
retries: 5
networks:
- fraiseql-network
redis:
image: redis:7-alpine
container_name: fraiseql-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- fraiseql-network
nginx:
image: nginx:alpine
container_name: fraiseql-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- fraiseql
networks:
- fraiseql-network
volumes:
postgres_data:
redis_data:
networks:
fraiseql-network:
driver: bridge
Kubernetes Deployment¶
Complete Deployment Manifest¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: fraiseql
namespace: production
labels:
app: fraiseql
tier: backend
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: fraiseql
template:
metadata:
labels:
app: fraiseql
version: v1.0.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: fraiseql
containers:
- name: fraiseql
image: gcr.io/your-project/fraiseql:1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8000
protocol: TCP
- name: metrics
containerPort: 8000
# Environment from ConfigMap
envFrom:
- configMapRef:
name: fraiseql-config
# Secrets
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: database-password
- name: SENTRY_DSN
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: sentry-dsn
# Resource requests/limits
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
# Liveness probe
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
# Readiness probe
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 2
# Startup probe
startupProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 0
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
# Security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop:
- ALL
# Graceful shutdown
terminationGracePeriodSeconds: 30
# Pod-level security
securityContext:
fsGroup: 1000
---
apiVersion: v1
kind: Service
metadata:
name: fraiseql
namespace: production
labels:
app: fraiseql
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
- name: metrics
port: 8000
targetPort: metrics
selector:
app: fraiseql
Horizontal Pod Autoscaler¶
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: fraiseql
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: fraiseql
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
- type: Pods
pods:
metric:
name: graphql_requests_per_second
target:
type: AverageValue
averageValue: "100"
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 50
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
Environment Configuration¶
Environment Variables¶
# .env.production
# Core
FRAISEQL_ENVIRONMENT=production
FRAISEQL_APP_NAME="FraiseQL API"
FRAISEQL_APP_VERSION=1.0.0
# Database
FRAISEQL_DATABASE_URL=postgresql://user:password@localhost:5432/fraiseql
FRAISEQL_DATABASE_POOL_SIZE=20
FRAISEQL_DATABASE_MAX_OVERFLOW=10
FRAISEQL_DATABASE_POOL_TIMEOUT=30
# Security
FRAISEQL_AUTH_ENABLED=true
FRAISEQL_AUTH_PROVIDER=auth0
FRAISEQL_AUTH0_DOMAIN=your-tenant.auth0.com
FRAISEQL_AUTH0_API_IDENTIFIER=https://api.yourapp.com
# Performance
FRAISEQL_JSON_PASSTHROUGH_ENABLED=true
FRAISEQL_TURBO_ROUTER_ENABLED=true
FRAISEQL_ENABLE_QUERY_CACHING=true
FRAISEQL_CACHE_TTL=300
# GraphQL
FRAISEQL_INTROSPECTION_POLICY=disabled
FRAISEQL_ENABLE_PLAYGROUND=false
FRAISEQL_MAX_QUERY_DEPTH=10
FRAISEQL_QUERY_TIMEOUT=30
# Monitoring
FRAISEQL_ENABLE_METRICS=true
FRAISEQL_METRICS_PATH=/metrics
SENTRY_DSN=https://...@sentry.io/...
SENTRY_ENVIRONMENT=production
SENTRY_TRACES_SAMPLE_RATE=0.1
# CORS
FRAISEQL_CORS_ENABLED=true
FRAISEQL_CORS_ORIGINS=https://app.yourapp.com,https://www.yourapp.com
# Rate Limiting
FRAISEQL_RATE_LIMIT_ENABLED=true
FRAISEQL_RATE_LIMIT_REQUESTS_PER_MINUTE=60
FRAISEQL_RATE_LIMIT_REQUESTS_PER_HOUR=1000
Kubernetes Secrets¶
apiVersion: v1
kind: Secret
metadata:
name: fraiseql-secrets
namespace: production
type: Opaque
stringData:
database-password: "your-secure-password"
sentry-dsn: "https://...@sentry.io/..."
auth0-client-secret: "your-auth0-secret"
Database Migrations¶
Migration Strategy¶
# migrations/run_migrations.py
import asyncio
import sys
from alembic import command
from alembic.config import Config
async def run_migrations():
"""Run database migrations before deployment."""
alembic_cfg = Config("alembic.ini")
try:
# Check current version
command.current(alembic_cfg)
# Run migrations
command.upgrade(alembic_cfg, "head")
print("✓ Migrations completed successfully")
return 0
except Exception as e:
print(f"✗ Migration failed: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(asyncio.run(run_migrations()))
Kubernetes Init Container¶
spec:
initContainers:
- name: migrate
image: gcr.io/your-project/fraiseql:1.0.0
command: ["python", "migrations/run_migrations.py"]
envFrom:
- configMapRef:
name: fraiseql-config
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: database-password
Health Checks¶
Health Check Endpoint¶
from fraiseql.monitoring import HealthCheck, CheckResult, HealthStatus
from fraiseql.monitoring.health_checks import check_database, check_pool_stats
# Create health check
health = HealthCheck()
health.add_check("database", check_database)
health.add_check("pool", check_pool_stats)
# FastAPI endpoints
from fastapi import FastAPI, Response
app = FastAPI()
@app.get("/health")
async def health_check():
"""Simple liveness check."""
return {"status": "healthy", "service": "fraiseql"}
@app.get("/ready")
async def readiness_check():
"""Comprehensive readiness check."""
result = await health.run_checks()
if result["status"] == "healthy":
return result
else:
return Response(
content=json.dumps(result),
status_code=503,
media_type="application/json"
)
Scaling Strategies¶
Horizontal Scaling¶
# Manual scaling
kubectl scale deployment fraiseql --replicas=10 -n production
# Check autoscaler status
kubectl get hpa fraiseql -n production
# View scaling events
kubectl describe hpa fraiseql -n production
Vertical Scaling¶
# Update resource limits
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 2Gi
# Apply changes
kubectl apply -f deployment.yaml
Database Connection Pool Scaling¶
# Adjust pool size based on replicas
# Rule: total_connections = replicas * pool_size
# PostgreSQL max_connections should be: total_connections + buffer
# 3 replicas * 20 connections = 60 total
# Set PostgreSQL max_connections = 100
config = FraiseQLConfig(
database_pool_size=20,
database_max_overflow=10
)
Zero-Downtime Deployment¶
Rolling Update Strategy¶
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Max pods above desired count
maxUnavailable: 0 # No downtime
Deployment Process¶
# 1. Build new image
docker build -t gcr.io/your-project/fraiseql:1.0.1 .
docker push gcr.io/your-project/fraiseql:1.0.1
# 2. Update deployment
kubectl set image deployment/fraiseql \
fraiseql=gcr.io/your-project/fraiseql:1.0.1 \
-n production
# 3. Watch rollout
kubectl rollout status deployment/fraiseql -n production
# 4. Verify new version
kubectl get pods -n production -l app=fraiseql
Blue-Green Deployment¶
# Green deployment (new version)
apiVersion: apps/v1
kind: Deployment
metadata:
name: fraiseql-green
spec:
replicas: 3
selector:
matchLabels:
app: fraiseql
version: green
template:
metadata:
labels:
app: fraiseql
version: green
spec:
containers:
- name: fraiseql
image: gcr.io/your-project/fraiseql:1.0.1
---
# Switch service to green
apiVersion: v1
kind: Service
metadata:
name: fraiseql
spec:
selector:
app: fraiseql
version: green # Changed from blue to green
Rollback Procedures¶
Kubernetes Rollback¶
# View rollout history
kubectl rollout history deployment/fraiseql -n production
# Rollback to previous version
kubectl rollout undo deployment/fraiseql -n production
# Rollback to specific revision
kubectl rollout undo deployment/fraiseql --to-revision=2 -n production
# Verify rollback
kubectl rollout status deployment/fraiseql -n production
Database Rollback¶
# migrations/rollback.py
from alembic import command
from alembic.config import Config
def rollback_migration(steps: int = 1):
"""Rollback database migrations."""
alembic_cfg = Config("alembic.ini")
command.downgrade(alembic_cfg, f"-{steps}")
print(f"✓ Rolled back {steps} migration(s)")
# Rollback one migration
rollback_migration(1)
Emergency Rollback Script¶
#!/bin/bash
# rollback.sh
set -e
echo "🚨 Emergency rollback initiated"
# 1. Rollback Kubernetes deployment
echo "Rolling back deployment..."
kubectl rollout undo deployment/fraiseql -n production
# 2. Wait for rollback
echo "Waiting for rollback to complete..."
kubectl rollout status deployment/fraiseql -n production
# 3. Verify health
echo "Checking health..."
kubectl exec -n production deployment/fraiseql -- curl -f http://localhost:8000/health
echo "✓ Rollback completed successfully"
Next Steps¶
- Monitoring - Metrics, logs, and alerting
- Security - Production security hardening
- Performance - Production optimization