Deployment
This guide covers running the x402 Facilitator in production - from Docker Compose on a single VM to a container-orchestrated cloud environment.
Quick Docker Compose
The repository ships with a docker-compose.yml that starts the facilitator alongside PostgreSQL and Redis.
# 1. Clone
git clone https://github.com/vitwit/facilitator-server.git
cd facilitator-server
# 2. Configure
cp config.example.yaml config.yaml
# Edit config.yaml - set secrets, RPC URLs, private keys
# 3. Build and run
docker-compose up -d
# 4. Check logs
docker-compose logs -f facilitatorThe server listens on http://localhost:8080 by default.
Building the Docker Image
docker build -t x402-facilitator:latest .Running the container
docker run -d \
--name x402-facilitator \
-p 8080:8080 \
-v /path/to/config.yaml:/app/config.yaml:ro \
x402-facilitator:latestMount
config.yamlas read-only. Never bake secrets into the image.
Production Checklist
Security
- [ ] Set all JWT secrets to ≥ 32 random bytes (
openssl rand -hex 32). - [ ] Set a strong
security.client_secret. - [ ] Set a strong, unique
admin.tokenand store it in a secrets manager. - [ ] Use
ssl_mode: "verify-full"for PostgreSQL in production. - [ ] Never commit
config.yamlto version control - add it to.gitignore. - [ ] Set
x402.allow_missing_client_auth: false. - [ ] Set
server.environment: "production"(disables Gin debug output).
Networking
- [ ] Put the facilitator behind a reverse proxy (Nginx, Caddy, or a cloud load balancer).
- [ ] Enforce HTTPS - terminate TLS at the proxy.
- [ ] Restrict
/adminroutes to internal networks only (via proxy ACLs or firewall rules). - [ ] Enable rate limiting (
rate_limit.enabled: true).
Database
- [ ] Use a managed PostgreSQL service (e.g. AWS RDS, GCP Cloud SQL, Supabase) for production.
- [ ] Enable automated backups.
- [ ] Run migrations before deploying a new version (see Migrations).
Observability
- [ ] Configure structured JSON logging (
logging.format: "json"). - [ ] Ship logs to a centralised log aggregator (Loki, Datadog, CloudWatch).
- [ ] Set up uptime monitoring on
GET /health.
Nginx Reverse Proxy
server {
listen 443 ssl http2;
server_name facilitator.yourdomain.com;
ssl_certificate /etc/ssl/certs/facilitator.crt;
ssl_certificate_key /etc/ssl/private/facilitator.key;
# Rate limiting
limit_req_zone $binary_remote_addr zone=facilitator:10m rate=10r/s;
location / {
limit_req zone=facilitator burst=20 nodelay;
proxy_pass http://127.0.0.1:8080;
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;
proxy_read_timeout 60s;
}
# Block admin from the public internet
location /admin {
deny all;
}
}Database Migrations
Migrations are stored in internal/database/migrations/ and run automatically on startup via golang-migrate. If you prefer to manage migrations manually:
# Install migrate CLI
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# Run migrations
migrate \
-path internal/database/migrations \
-database "postgres://user:pass@host:5432/dbname?sslmode=disable" \
upTo roll back:
migrate \
-path internal/database/migrations \
-database "postgres://user:pass@host:5432/dbname?sslmode=disable" \
down 1Kubernetes
A minimal Kubernetes deployment:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: x402-facilitator
spec:
replicas: 2
selector:
matchLabels:
app: x402-facilitator
template:
metadata:
labels:
app: x402-facilitator
spec:
containers:
- name: facilitator
image: x402-facilitator:latest
ports:
- containerPort: 8080
volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
readOnly: true
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
volumes:
- name: config
secret:
secretName: x402-facilitator-config
---
apiVersion: v1
kind: Service
metadata:
name: x402-facilitator
spec:
selector:
app: x402-facilitator
ports:
- port: 80
targetPort: 8080Store config.yaml as a Kubernetes Secret:
kubectl create secret generic x402-facilitator-config \
--from-file=config.yaml=./config.yamlHorizontal Scaling
The facilitator is stateless beyond the database and Redis. To scale horizontally:
- Run multiple instances behind a load balancer.
- Point all instances at the same PostgreSQL database.
- Point all instances at the same Redis instance (for distributed rate limiting - when implemented).
- Ensure all instances share the same JWT secrets so tokens issued by one instance are accepted by another.
Monitoring & Alerts
Health endpoint
Poll GET /health every 30 seconds. Alert if status is not "healthy" or the endpoint is unreachable.
Log-based alerts
Key log patterns to alert on:
| Pattern | Severity | Meaning |
|---|---|---|
SETTLEMENT_FAILED | Critical | On-chain settlement is failing. |
VERIFICATION_FAILED | Warning | Verification errors (may be malformed client requests). |
database: down | Critical | PostgreSQL connectivity lost. |
| HTTP 5xx spike | Warning | Unexpected server errors. |
Database size
Monitor the x402_metrics and metrics tables - they grow with every transaction. Set up archival or partitioning if you expect high volume.
Upgrading
- Pull the new image / build from the updated source.
- Run database migrations first (see above).
- Perform a rolling restart to avoid downtime.
# Docker Compose rolling update
docker-compose pull facilitator
docker-compose up -d --no-deps facilitator