Production Deployment¶
Deploy FraiseQL to production with Docker, monitoring, and security best practices.
Overview¶
Production deployment checklist: - Docker containerization - Database migrations - Environment configuration - Performance optimization - Monitoring and logging - Security hardening
Time: 60-90 minutes
Prerequisites¶
- Completed Blog API Tutorial
- Docker and Docker Compose installed
- Production database (PostgreSQL 14+)
- Domain name (for HTTPS)
Project Structure¶
myapp/
├── src/
│ ├── app.py
│ ├── models.py
│ ├── queries.py
│ └── mutations.py
├── db/
│ └── migrations/
│ ├── 001_initial_schema.sql
│ ├── 002_views.sql
│ └── 003_functions.sql
├── deploy/
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── nginx.conf
├── .env.example
├── pyproject.toml
└── README.md
Step 1: Dockerfile¶
# deploy/Dockerfile
FROM python:3.11-slim
# Install system dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Create app user
RUN useradd -m -u 1000 app && \
mkdir -p /app && \
chown -R app:app /app
WORKDIR /app
# Install Python dependencies
COPY --chown=app:app pyproject.toml ./
RUN pip install --no-cache-dir -e .
# Copy application
COPY --chown=app:app src/ ./src/
COPY --chown=app:app db/ ./db/
# Switch to app user
USER app
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8000/health')"
# Run application
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]
Step 2: Docker Compose¶
# deploy/docker-compose.yml
version: '3.8'
services:
db:
image: postgres:14-alpine
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/migrations:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 10s
timeout: 5s
retries: 5
api:
build:
context: ..
dockerfile: deploy/Dockerfile
environment:
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
ENV: production
LOG_LEVEL: info
RUST_ENABLED: "true"
APQ_ENABLED: "true"
APQ_STORAGE_BACKEND: postgresql
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
restart: unless-stopped
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
ports:
- "80:80"
- "443:443"
depends_on:
- api
restart: unless-stopped
volumes:
postgres_data:
Step 3: Nginx Configuration¶
# deploy/nginx.conf
events {
worker_connections 1024;
}
http {
upstream api {
server api:8000;
}
# Rate limiting
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
server {
listen 80;
server_name yourdomain.com;
# Redirect to HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
# SSL configuration
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Security headers
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# GraphQL endpoint
location /graphql {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check
location /health {
proxy_pass http://api;
access_log off;
}
}
}
Step 4: Application Configuration¶
# src/app.py
import os
from fraiseql import FraiseQL, FraiseQLConfig
from fraiseql.monitoring import setup_sentry, setup_prometheus
from psycopg_pool import AsyncConnectionPool
# Load environment
ENV = os.getenv("ENV", "development")
DATABASE_URL = os.getenv("DATABASE_URL")
# Configuration
config = FraiseQLConfig(
database_url=DATABASE_URL,
# Performance
rust_enabled=os.getenv("RUST_ENABLED", "true").lower() == "true",
apq_enabled=os.getenv("APQ_ENABLED", "true").lower() == "true",
apq_storage_backend=os.getenv("APQ_STORAGE_BACKEND", "postgresql"),
enable_turbo_router=True,
json_passthrough_enabled=True,
# Security
enable_playground=(ENV != "production"),
complexity_enabled=True,
complexity_max_score=1000,
query_depth_limit=10,
# Monitoring
enable_logging=True,
log_level=os.getenv("LOG_LEVEL", "info"),
)
# Initialize app
app = FraiseQL(config=config)
# Connection pool
pool = AsyncConnectionPool(
conninfo=DATABASE_URL,
min_size=5,
max_size=20,
timeout=5.0
)
# Monitoring setup
if ENV == "production":
setup_sentry(
dsn=os.getenv("SENTRY_DSN"),
environment=ENV,
traces_sample_rate=0.1
)
setup_prometheus(app)
# Health check endpoint
@app.get("/health")
async def health_check():
"""Health check for load balancer."""
async with pool.connection() as conn:
await conn.execute("SELECT 1")
return {"status": "healthy"}
# Graceful shutdown
@app.on_event("shutdown")
async def shutdown():
await pool.close()
Step 5: Environment Variables¶
# .env.example
# Database
DB_NAME=myapp_production
DB_USER=myapp
DB_PASSWORD=<secure-password>
DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
# Application
ENV=production
LOG_LEVEL=info
SECRET_KEY=<generate-with-openssl-rand-hex-32>
# Performance
RUST_ENABLED=true
APQ_ENABLED=true
APQ_STORAGE_BACKEND=postgresql
# Monitoring
SENTRY_DSN=https://...@sentry.io/...
# Security
ALLOWED_HOSTS=yourdomain.com
Step 6: Database Migrations¶
# db/migrations/001_initial_schema.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
-- Tables
CREATE TABLE tb_user (...);
CREATE TABLE tb_post (...);
-- Indexes
CREATE INDEX idx_post_author ON tb_post(fk_author);
Migration Script:
#!/bin/bash
# scripts/migrate.sh
set -e
DATABASE_URL=${DATABASE_URL:-postgresql://localhost/myapp}
echo "Running migrations..."
for migration in db/migrations/*.sql; do
echo "Applying $migration"
psql "$DATABASE_URL" -f "$migration"
done
echo "Migrations complete!"
Step 7: Deploy to Production¶
Option A: Docker Compose¶
# 1. Clone repository
git clone https://github.com/yourorg/myapp.git
cd myapp
# 2. Configure environment
cp .env.example .env
nano .env # Edit with production values
# 3. Start services
docker-compose -f deploy/docker-compose.yml up -d
# 4. Check health
curl https://yourdomain.com/health
# 5. View logs
docker-compose -f deploy/docker-compose.yml logs -f api
Option B: Kubernetes¶
# deploy/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fraiseql-api
spec:
replicas: 3
selector:
matchLabels:
app: fraiseql-api
template:
metadata:
labels:
app: fraiseql-api
spec:
containers:
- name: api
image: yourorg/myapp:latest
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
- name: ENV
value: "production"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
Step 8: Monitoring¶
Prometheus Metrics¶
# src/monitoring.py
from prometheus_client import Counter, Histogram, Gauge
# Request metrics
http_requests_total = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
query_duration_seconds = Histogram(
'graphql_query_duration_seconds',
'GraphQL query duration',
['operation']
)
db_pool_connections = Gauge(
'db_pool_connections',
'Active database connections'
)
# Middleware
@app.middleware("http")
async def metrics_middleware(request, call_next):
start_time = time.time()
response = await call_next(request)
duration = time.time() - start_time
query_duration_seconds.labels(
operation=request.url.path
).observe(duration)
http_requests_total.labels(
method=request.method,
endpoint=request.url.path,
status=response.status_code
).inc()
return response
Grafana Dashboard¶
{
"dashboard": {
"title": "FraiseQL Monitoring",
"panels": [
{
"title": "Request Rate",
"targets": [
{
"expr": "rate(http_requests_total[5m])"
}
]
},
{
"title": "Query Duration P95",
"targets": [
{
"expr": "histogram_quantile(0.95, graphql_query_duration_seconds)"
}
]
},
{
"title": "Database Connections",
"targets": [
{
"expr": "db_pool_connections"
}
]
}
]
}
}
Step 9: Security Checklist¶
- [ ] Use HTTPS only (TLS 1.2+)
- [ ] Disable GraphQL Playground in production
- [ ] Implement rate limiting
- [ ] Set query complexity limits
- [ ] Use environment variables for secrets
- [ ] Enable CORS only for known origins
- [ ] Implement authentication middleware
- [ ] Add security headers (CSP, HSTS)
- [ ] Run database as non-root user
- [ ] Use prepared statements (automatic with FraiseQL)
- [ ] Enable audit logging
- [ ] Set up alerts for unusual activity
Step 10: Performance Optimization¶
Database Tuning¶
-- PostgreSQL configuration (postgresql.conf)
shared_buffers = 256MB
effective_cache_size = 1GB
work_mem = 16MB
maintenance_work_mem = 128MB
max_connections = 100
-- Connection pooling
max_pool_size = 20
min_pool_size = 5
-- Enable query logging
log_min_duration_statement = 100 # Log queries > 100ms
Application Tuning¶
config = FraiseQLConfig(
# Layer 0: Rust (10-80x faster)
rust_enabled=True,
# Layer 1: APQ (5-10x faster)
apq_enabled=True,
apq_storage_backend="postgresql",
# Layer 2: TurboRouter (3-5x faster)
enable_turbo_router=True,
turbo_router_cache_size=500,
# Layer 3: JSON Passthrough (2-3x faster)
json_passthrough_enabled=True,
# Combined: 0.5-2ms cached responses
)
Troubleshooting¶
High Memory Usage¶
# Check connection pool
docker exec api python -c "
from src.app import pool
print(f'Pool size: {pool.get_stats()}')
"
# Adjust pool size
MAX_POOL_SIZE=10 docker-compose restart api
Slow Queries¶
# Enable query logging
psql $DATABASE_URL -c "ALTER SYSTEM SET log_min_duration_statement = 100;"
psql $DATABASE_URL -c "SELECT pg_reload_conf();"
# View slow queries
docker-compose logs api | grep "duration:"
Database Connection Errors¶
# Check database health
docker-compose exec db pg_isready
# Check connection string
docker-compose exec api env | grep DATABASE_URL
Production Checklist¶
Before Launch¶
- [ ] Run full test suite
- [ ] Load test with realistic traffic
- [ ] Set up monitoring alerts
- [ ] Configure backups
- [ ] Document rollback procedure
- [ ] Test health check endpoints
- [ ] Verify SSL certificates
- [ ] Review security settings
After Launch¶
- [ ] Monitor error rates
- [ ] Check query performance
- [ ] Verify cache hit rates
- [ ] Monitor database connections
- [ ] Review security logs
- [ ] Test scaling
See Also¶
- Performance - Optimization techniques
- Monitoring - Observability setup
- Security - Security hardening
- Database Patterns - Production patterns