- Docker Compose (Production)
- Kubernetes
- KEDA Autoscaling
- Rolling Updates and Rollbacks
- Monitoring in Production
- Database Backup and Restore
Docker Compose production mode uses docker-compose.prod.yml which removes dev-only services (Mailpit, Mongo Express, Redpanda Console) and enables TLS through NGINX.
cp .env.example .env
# Edit .env with production values:
# - Strong JWT_SECRET (min 64 chars)
# - Secure POSTGRES_PASSWORD, MONGO_INITDB_ROOT_PASSWORD, REDIS_PASSWORD
# - Real STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET
# - Production SMTP settings (replace Mailpit with real SMTP)
# - Set EMAIL_FROM to your domain
nano .envmake build
# Equivalent to: docker compose -f docker-compose.yml -f docker-compose.prod.yml buildmake prod-up
# Equivalent to: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -dmake healthCreate /etc/nginx/conf.d/ticketflow.conf:
upstream ticketflow_gateway {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name tickets.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name tickets.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/tickets.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tickets.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://ticketflow_gateway;
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;
proxy_cache_bypass $http_upgrade;
}
}Obtain a certificate with Certbot:
certbot --nginx -d tickets.yourdomain.comTwo sub-sections below: Local (OrbStack / Docker Desktop) for development on a single machine, and Production cluster for a real environment.
This is the tested path for running the full stack on a single developer machine. All images are built locally; no registry is required.
| Tool | Install |
|---|---|
| OrbStack or Docker Desktop | orbstack.dev — enable Kubernetes in settings |
kubectl |
bundled with OrbStack; or brew install kubectl |
make |
brew install make |
docker |
bundled with OrbStack / Docker Desktop |
Memory: The full stack needs approximately 6–7 GB of RAM allocated to the VM. On an 8 GB machine this leaves very little headroom. See Memory-constrained startup below.
make k8s-build
# Builds: gateway, user-service, event-service, booking-service,
# inventory-service, payment-service, notification-service, frontendAll images are tagged ticketflow/<service>:latest and stored in the local Docker daemon that the cluster already uses — no push needed.
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/secrets/Infra services must be healthy before app services start. Deploy and wait for each layer:
# Persistent stores and message broker
kubectl apply -f k8s/infra/ -n ticketflow --recursive
# Wait for the critical path: ZooKeeper → Kafka → Postgres → MongoDB → Redis
kubectl wait --for=condition=ready pod/zookeeper-0 -n ticketflow --timeout=120s
kubectl wait --for=condition=ready pod/kafka-0 -n ticketflow --timeout=120s
kubectl wait --for=condition=ready pod/postgres-0 -n ticketflow --timeout=120s
kubectl wait --for=condition=ready pod/mongodb-0 -n ticketflow --timeout=120s
kubectl wait --for=condition=ready pod/redis-0 -n ticketflow --timeout=120skubectl apply -f k8s/apps/ -n ticketflow --recursivekubectl apply -f k8s/monitoring/ -n ticketflow --recursivekubectl exec -n ticketflow deployment/event-service -- python -m app.seedDemo credentials: test@ticketflow.dev / Test1234!
make health
# All six services should report "up"All user-facing services are exposed as NodePort — no port-forwarding needed.
Application
| Service | URL | NodePort |
|---|---|---|
| Frontend (React) | http://localhost:30080 | 30080 → pod:80 |
| API Gateway | http://localhost:30000 | 30000 → pod:3000 |
Observability
| Tool | URL | NodePort | Credentials |
|---|---|---|---|
| Grafana | http://localhost:30030 | 30030 → pod:3000 | admin / admin |
| Prometheus | http://localhost:30090 | 30090 → pod:9090 | — |
| Jaeger | http://localhost:30686 | 30686 → pod:16686 | — |
Loki is internal-only (scraped by Grafana automatically via the provisioned datasource — use Explore → Loki in Grafana to query logs).
On a machine with ≤ 8 GB RAM, deploying all services at once can OOM the cluster VM. To avoid this:
- Deploy and wait for infra first (step 3 above).
- Then deploy app services (step 4). If pods are
PendingwithInsufficient memory, the HPAs have scaled to 2 replicas. Patch them all down to 1:
for svc in booking-service user-service event-service gateway inventory-service; do
kubectl patch hpa $svc -n ticketflow --type=merge -p '{"spec":{"minReplicas":1}}'
done
kubectl scale deployment \
booking-service user-service event-service gateway \
inventory-service payment-service notification-service frontend \
-n ticketflow --replicas=1If the cluster VM is restarted (OOM kill, orbctl stop, etc.), Kafka will fail with:
Fatal error: Invalid cluster.id … Expected X, but read Y
ZooKeeper reinitialised with a new cluster ID but the Kafka PVC still has the old one. Fix:
kubectl scale statefulset kafka zookeeper -n ticketflow --replicas=0
kubectl delete pvc kafka-data-kafka-0 zookeeper-data-zookeeper-0 -n ticketflow
kubectl scale statefulset zookeeper -n ticketflow --replicas=1
kubectl wait --for=condition=ready pod/zookeeper-0 -n ticketflow --timeout=60s
kubectl scale statefulset kafka -n ticketflow --replicas=1
kubectl wait --for=condition=ready pod/kafka-0 -n ticketflow --timeout=90sThen restart any app pods that started before Kafka was ready:
kubectl rollout restart deployment \
booking-service event-service inventory-service payment-service \
notification-service -n ticketflow| Tool | Install |
|---|---|
kubectl |
kubernetes.io/docs |
helm |
brew install helm |
| KEDA operator | Installed via Helm (see below) |
| Container registry | Docker Hub, GCR, ECR, or GHCR |
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda \
--namespace keda \
--create-namespaceVerify:
kubectl get pods -n kedakubectl apply -f k8s/namespace.yaml
cp k8s/secrets/app-secrets.yaml.example k8s/secrets/app-secrets.yaml
# Edit k8s/secrets/app-secrets.yaml with production values (base64-encoded)
kubectl apply -f k8s/secrets/Encode a value:
echo -n "your-secret-value" | base64# Tag and push (replace <registry> with your registry prefix)
for svc in gateway user-service event-service booking-service \
inventory-service payment-service notification-service frontend; do
docker tag ticketflow/$svc:latest <registry>/ticketflow-$svc:latest
docker push <registry>/ticketflow-$svc:latest
doneUpdate image references in the manifests:
find k8s/ -name "deployment.yaml" \
-exec sed -i 's|image: ticketflow/|image: <registry>/ticketflow-|g' {} +make k8s-applyChange the frontend and gateway services back to ClusterIP for production (NodePort is only appropriate locally), then apply an Ingress. Install cert-manager if not present:
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set installCRDs=trueExample Ingress (edit hostnames):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ticketflow
namespace: ticketflow
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts: [tickets.yourdomain.com, api.yourdomain.com]
secretName: ticketflow-tls
rules:
- host: tickets.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: frontend, port: { number: 80 } }
- host: api.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: gateway, port: { number: 3000 } }KEDA ScaledObjects are defined in k8s/keda/scaledobjects.yaml. Each application service that consumes Kafka has a ScaledObject:
# Example: booking-service ScaledObject
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: booking-service-scaler
namespace: ticketflow
spec:
scaleTargetRef:
name: booking-service
minReplicaCount: 1
maxReplicaCount: 10
triggers:
- type: kafka
metadata:
bootstrapServers: kafka:9092
consumerGroup: booking-service
topic: ticketflow.seats.locked
lagThreshold: "50"
offsetResetPolicy: latest- KEDA polls the Kafka consumer group lag every 30 seconds.
- If lag on
ticketflow.seats.lockedexceeds 50 messages, KEDA adds replicas. - New replicas join the
booking-serviceconsumer group and Kafka redistributes partitions. - As lag drops, KEDA scales down to
minReplicaCount.
kubectl get scaledobjects -n ticketflow
kubectl describe scaledobject booking-service-scaler -n ticketflow- Build and push a new image:
docker build -t yourreg/ticketflow-booking-service:v1.2.0 services/booking-service/
docker push yourreg/ticketflow-booking-service:v1.2.0- Update the image in the deployment:
kubectl set image deployment/booking-service \
booking-service=yourreg/ticketflow-booking-service:v1.2.0 \
-n ticketflow- Monitor the rollout:
kubectl rollout status deployment/booking-service -n ticketflow# Rollback to the previous version
kubectl rollout undo deployment/booking-service -n ticketflow
# Rollback to a specific revision
kubectl rollout history deployment/booking-service -n ticketflow
kubectl rollout undo deployment/booking-service --to-revision=3 -n ticketflowAll deployments use RollingUpdate strategy with:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0This ensures at least one healthy pod is running throughout the update.
Prometheus scrapes metrics from all services via annotations:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3000"
prometheus.io/path: "/metrics"Access: http://localhost:30090
Pre-built dashboards available at infra/grafana/dashboards/:
overview.json— platform-wide health: request rate, error rate, saga throughput.booking-saga.json— saga step latencies, failure rates per step.kafka.json— consumer group lag per topic.jvm.json— JVM heap, GC, thread count (Java services).python.json— Python process memory, CPU, event loop lag.
Access: http://localhost:30030 (admin / admin)
Import dashboards: Dashboards → Import → Upload JSON file
Trace a booking end-to-end:
http://localhost:30686
# Search by Service: "gateway", Operation: "POST /api/bookings"
# The trace shows spans across: gateway → booking-service → kafka → inventory-service → payment-service → notification-service
# Tail logs for a service
kubectl logs -f deployment/booking-service -n ticketflow
# Last 100 lines
kubectl logs --tail=100 deployment/payment-service -n ticketflow
# All pods in a deployment
kubectl logs -f -l app=booking-service -n ticketflow
# Query logs in Grafana Loki (no port-forward needed)
# Open http://localhost:30030 → Explore → Loki → {app="booking-service"} |= "ERROR"# Backup
kubectl exec -n ticketflow deployment/postgres -- \
pg_dumpall -U ticketflow > backup-$(date +%Y%m%d).sql
# Restore
kubectl exec -i -n ticketflow deployment/postgres -- \
psql -U ticketflow < backup-20240101.sql# Backup
kubectl exec -n ticketflow deployment/mongodb -- \
mongodump --uri="mongodb://ticketflow:$MONGO_PASSWORD@localhost:27017" \
--archive > mongo-backup-$(date +%Y%m%d).archive
# Restore
kubectl exec -i -n ticketflow deployment/mongodb -- \
mongorestore --uri="mongodb://ticketflow:$MONGO_PASSWORD@localhost:27017" \
--archive < mongo-backup-20240101.archiveA Kubernetes CronJob for daily backups is provided in k8s/infrastructure/backup-cronjob.yaml. It runs at 02:00 UTC and uploads backups to an S3-compatible bucket.