Skip to content

Deployment

This guide covers deploying FraiseQL to production environments, including Docker, cloud platforms, and operational best practices.

[project]
name = "my-api"
version = "1.0.0"
[server]
host = "0.0.0.0"
port = 8080
[server.cors]
origins = ["https://app.example.com"]
credentials = true
[database]
url = "${DATABASE_URL}"
pool_min = 10
pool_max = 100
connect_timeout_ms = 5000
idle_timeout_ms = 600000
ssl_mode = "require"
[security.error_sanitization]
enabled = true
generic_messages = true
leak_sensitive_details = false
[security.rate_limiting]
enabled = true
auth_start_max_requests = 100
auth_start_window_secs = 60
failed_login_max_requests = 5
failed_login_window_secs = 3600

For JWT token validation, configure JWT_SECRET and JWT_ALGORITHM environment variables (see Environment Variables below). For OIDC/OAuth2 flows, use the [security.pkce] TOML section with client_id, issuer_url, and redirect_uri.

Required environment variables for production:

Terminal window
# Database
DATABASE_URL=postgresql://user:pass@host:5432/dbname?sslmode=require
# JWT authentication
JWT_SECRET=your-256-bit-secret-key # required if using JWT auth
JWT_ALGORITHM=HS256 # HS256 | RS256 | ES256 (default: HS256)
# Logging (standard Rust env var)
RUST_LOG=info
# OpenTelemetry tracing (standard OTel env vars)
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.example.com:4317
OTEL_SERVICE_NAME=fraiseql-api

All three transports — GraphQL, REST, and gRPC — are served from the same port in the same binary. There is no separate deployment for each transport, and no additional process to manage.

FraiseQL is distributed as a pre-built Docker image — no Rust toolchain required. Use the official image as your base and copy in your schema. One container serves all transports (GraphQL, REST, and gRPC):

FROM ghcr.io/fraiseql/server:latest
WORKDIR /app
# Copy schema source and config
COPY fraiseql.toml .
COPY schema.py .
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -qO- http://localhost:8080/health || exit 1
EXPOSE 8080
CMD ["fraiseql", "run"]

For CI/CD pipelines where you want to pre-compile the schema to a static artifact:

# Stage 1: compile schema to schema.compiled.json
FROM ghcr.io/fraiseql/server:latest AS builder
WORKDIR /app
COPY fraiseql.toml .
COPY schema.py .
RUN fraiseql compile
# Stage 2: minimal runtime image with compiled schema
FROM ghcr.io/fraiseql/server:latest
WORKDIR /app
COPY --from=builder /app/schema.compiled.json .
COPY fraiseql.toml .
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -qO- http://localhost:8080/health || exit 1
EXPOSE 8080
CMD ["fraiseql", "run", "schema.compiled.json"]
version: "3.8"
services:
api:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=${DATABASE_URL}
- JWT_SECRET=${JWT_SECRET}
- RUST_LOG=info
depends_on:
- postgres
deploy:
replicas: 3
resources:
limits:
cpus: "2"
memory: 1G
reservations:
cpus: "0.5"
memory: 256M
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
apiVersion: apps/v1
kind: Deployment
metadata:
name: fraiseql-api
labels:
app: fraiseql-api
spec:
replicas: 3
selector:
matchLabels:
app: fraiseql-api
template:
metadata:
labels:
app: fraiseql-api
spec:
containers:
- name: api
image: your-registry/fraiseql-api:latest
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: database-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: jwt-secret
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "2000m"
memory: "1Gi"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: fraiseql-api
spec:
selector:
app: fraiseql-api
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fraiseql-api
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- api.example.com
secretName: fraiseql-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: fraiseql-api
port:
number: 80
apiVersion: v1
kind: Secret
metadata:
name: fraiseql-secrets
type: Opaque
stringData:
database-url: postgresql://user:pass@postgres:5432/mydb
jwt-secret: your-256-bit-secret # injected as JWT_SECRET env var

Task Definition:

{
"family": "fraiseql-api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"containerDefinitions": [
{
"name": "api",
"image": "your-ecr/fraiseql-api:latest",
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"environment": [
{
"name": "RUST_LOG",
"value": "info"
}
],
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:fraiseql/database-url"
},
{
"name": "JWT_SECRET",
"valueFrom": "arn:aws:secretsmanager:region:account:secret:fraiseql/jwt-secret"
}
],
"healthCheck": {
"command": ["CMD-SHELL", "wget -qO- http://localhost:8080/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/fraiseql-api",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
  1. Test migrations locally against a copy of production data
  2. Create a backup before migrating
  3. Run migrations during low-traffic periods
  4. Monitor for errors after migration
Terminal window
# Create backup
pg_dump $DATABASE_URL > backup-$(date +%Y%m%d).sql
# Run migrations (use your SQL migration tool: Flyway, Liquibase, sqitch, golang-migrate, etc.)
# FraiseQL does not bundle a migration runner — manage schema changes separately

For zero-downtime deployments:

  1. Add columns as nullable first
  2. Deploy code that handles both old and new schema
  3. Backfill data for new columns
  4. Add constraints after data is backfilled
  5. Remove old code paths

FraiseQL exposes health endpoints:

EndpointDescription
/healthOverall health status
/health/liveLiveness probe (is process running)
/health/readyReadiness probe (can accept traffic)
Terminal window
curl http://localhost:8080/health
# {"status": "healthy", "database": "connected", "version": "1.0.0"}

In federation deployments, /health includes a federation field with per-subgraph circuit breaker state:

{
"status": "healthy",
"database": "connected",
"federation": {
"subgraphs": [
{ "name": "Order", "state": "closed" },
{ "name": "Product", "state": "open" }
]
}
}

"closed" = healthy, "open" = tripped (requests rejected), "half_open" = in recovery. Alert when any subgraph enters the "open" state.

FraiseQL is stateless and scales horizontally:

# Kubernetes HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: fraiseql-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: fraiseql-api
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

For high-scale deployments, use external connection pooling:

# PgBouncer sidecar
- name: pgbouncer
image: edoburu/pgbouncer:1.21.0
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: fraiseql-secrets
key: database-url
- name: POOL_MODE
value: transaction
- name: MAX_CLIENT_CONN
value: "1000"
- name: DEFAULT_POOL_SIZE
value: "20"
  • TLS enabled for all traffic (via load balancer or [server.tls])
  • JWT secrets are strong (JWT_SECRET env var, 256+ bits)
  • Database credentials use secrets management
  • Network policies restrict pod-to-pod traffic
  • Rate limiting enabled ([security.rate_limiting] in fraiseql.toml)
  • Error sanitization enabled ([security.error_sanitization] in fraiseql.toml)
  • CORS origins explicitly listed ([server.cors] in fraiseql.toml)
  • SQL injection prevented (parameterized queries — FraiseQL does this automatically)
# ServiceMonitor for Prometheus Operator
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: fraiseql-api
spec:
selector:
matchLabels:
app: fraiseql-api
endpoints:
- port: http
path: /metrics
interval: 30s
MetricDescriptionAlert Threshold
fraiseql_requests_totalTotal requests-
fraiseql_request_duration_secondsRequest latencyp99 > 500ms
fraiseql_db_pool_sizeConnection pool size> 80% of max
fraiseql_errors_totalError count> 10/min

Performance Optimization

Optimize FraiseQL for production load with query tuning and caching. Performance Guide

Troubleshooting

Debug production issues including connection errors and slow queries. Troubleshooting Guide

Security Hardening

Additional security configuration for production deployments. Security Guide

Cloud Deployment Guides

Step-by-step guides for AWS, GCP, and Azure deployments. Deployment Overview