Manifests
This page covers the app repo structure and key manifest configurations for the Cal.com scheduling stack.
Meeting scheduling series
- Meeting scheduling
- Architecture
- Manifests - You are here
- Flux integration
- Operations
App repo structure
The app repo contains all Kubernetes manifests for Cal.com and PostgreSQL.
your-org/calcom/
├── .sops.yaml # SOPS encryption config
└── k8s/prod/
├── kustomization.yaml
├── 10-secret-db.enc.yaml # SOPS encrypted PostgreSQL creds
├── 11-secret-calcom.enc.yaml # SOPS encrypted app secrets
├── 20-db-statefulset.yaml # PostgreSQL with NFS patterns
├── 30-calcom-deployment.yaml # Cal.com with init containers
├── 40-ingress.yaml # Ingress for cal.example.com
├── 50-secret-cfapi-token.enc.yaml # Cloudflare API token
├── 60-originissuer.yaml # Cloudflare Origin CA issuer
└── 70-certificate.yaml # TLS certificate
Email relay is deployed separately in the email-relay namespace. See Email relay for details.
SOPS configuration
The .sops.yaml at the repo root configures encryption for secrets:
# .sops.yaml
creation_rules:
- path_regex: k8s/.*\.enc\.ya?ml$
encrypted_regex: '^(data|stringData)$'
age: ['age1your-public-key-here...']
Encrypt all secrets before committing. Pushing unencrypted secrets to Git exposes credentials.
# Encrypt secrets
sops -e -i k8s/prod/10-secret-db.enc.yaml
sops -e -i k8s/prod/11-secret-calcom.enc.yaml
sops -e -i k8s/prod/50-secret-cfapi-token.enc.yaml
# Edit later
sops k8s/prod/11-secret-calcom.enc.yaml
Database secret
PostgreSQL credentials stored as a SOPS-encrypted secret.
# k8s/prod/10-secret-db.enc.yaml (before encryption)
apiVersion: v1
kind: Secret
metadata:
name: calcom-db
namespace: calcom
type: Opaque
stringData:
POSTGRES_USER: calcom
POSTGRES_PASSWORD: "your-secure-password"
POSTGRES_DB: calcom
Cal.com secret
Application secrets including database URL and email sender.
# k8s/prod/11-secret-calcom.enc.yaml (before encryption)
apiVersion: v1
kind: Secret
metadata:
name: calcom-secret
namespace: calcom
type: Opaque
stringData:
DATABASE_URL: "postgresql://calcom:password@calcom-db:5432/calcom"
DATABASE_DIRECT_URL: "postgresql://calcom:password@calcom-db:5432/calcom"
NEXTAUTH_SECRET: "generate-with-openssl-rand-base64-32"
CALENDSO_ENCRYPTION_KEY: "generate-with-openssl-rand-base64-24"
EMAIL_FROM: "you@yourdomain.com"
# Google Calendar and Zoom: configured via Admin UI (not env vars)
Google Calendar and Zoom credentials are NOT stored in Kubernetes secrets. They are configured through the Cal.com admin UI (Settings → Admin → Apps) after deployment. The custom startup command (see deployment section) ensures these settings persist across pod restarts.
If your PostgreSQL password contains special characters (/, =, +), URL-encode them in the connection string:
/→%2F=→%3D+→%2B
PostgreSQL StatefulSet
PostgreSQL runs as a StatefulSet with NFS-specific patterns for stability.
# k8s/prod/20-db-statefulset.yaml (key sections)
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: calcom-db
namespace: calcom
spec:
serviceName: calcom-db
replicas: 1
selector:
matchLabels:
app: calcom-db
template:
metadata:
labels:
app: calcom-db
spec:
securityContext:
fsGroup: 999 # PostgreSQL user GID for NFS permissions
containers:
- name: postgres
image: postgres:16.4-alpine
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: calcom-db
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
# Probes use -h localhost to avoid Service resolution deadlock
startupProbe:
exec:
command: ["pg_isready", "-U", "calcom", "-h", "localhost"]
failureThreshold: 30 # 5 min timeout for crash recovery
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
command: ["pg_isready", "-U", "calcom", "-h", "localhost"]
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-U", "calcom", "-h", "localhost"]
failureThreshold: 6 # Prevents false restarts
periodSeconds: 10
timeoutSeconds: 5
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: nfs-client
resources:
requests:
storage: 5Gi
PostgreSQL NFS patterns
These patterns are critical for stability on NFS-backed storage:
| Pattern | Value | Reason |
|---|---|---|
securityContext.fsGroup | 999 | PostgreSQL user GID for NFS file permissions |
| Probe command | -h localhost | Avoids Service resolution deadlock (probe resolves Service name which has no endpoints until pod is Ready) |
startupProbe.failureThreshold | 30 | 5 minute timeout for crash recovery on slow NFS |
timeoutSeconds | 5 | Tolerance for NFS latency |
livenessProbe.failureThreshold | 6 | Prevents false restarts during NFS hiccups |
Cal.com Deployment
Cal.com runs as a Deployment with init containers for database migrations.
# k8s/prod/30-calcom-deployment.yaml (key sections)
apiVersion: apps/v1
kind: Deployment
metadata:
name: calcom
namespace: calcom
spec:
replicas: 1
selector:
matchLabels:
app: calcom
template:
metadata:
labels:
app: calcom
spec:
initContainers:
# Wait for PostgreSQL to be ready
- name: wait-for-postgres
image: postgres:16.4-alpine
command:
- sh
- -c
- |
until pg_isready -h calcom-db -U calcom; do
echo "Waiting for PostgreSQL..."
sleep 2
done
envFrom:
- secretRef:
name: calcom-db
# Run Prisma migrations (idempotent)
- name: migrate
image: calcom/cal.com:v6.1.3
command: ["npx", "prisma", "migrate", "deploy"]
envFrom:
- secretRef:
name: calcom-secret
containers:
- name: calcom
image: calcom/cal.com:v6.1.3
# Custom command to skip seed-app-store.ts (preserves Admin UI app settings)
command: ["/bin/sh", "-c"]
args:
- |
scripts/replace-placeholder.sh "$BUILT_NEXT_PUBLIC_WEBAPP_URL" "$NEXT_PUBLIC_WEBAPP_URL"
exec yarn start
ports:
- containerPort: 3000
env:
# Memory configuration
- name: NODE_OPTIONS
value: "--max-old-space-size=1536"
- name: NEXT_PUBLIC_WEBAPP_URL
value: "https://cal.example.com"
- name: NEXTAUTH_URL
value: "https://cal.example.com"
# Required for Cal.com v6+ hostname validation
- name: ALLOWED_HOSTNAMES
value: '["example.com"]'
# Email via shared email-relay namespace
- name: EMAIL_SERVER_HOST
value: "email-relay.email-relay.svc.cluster.local"
- name: EMAIL_SERVER_PORT
value: "25"
envFrom:
- secretRef:
name: calcom-secret
# Cal.com lacks /api/health - use TCP socket probes
readinessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 60
periodSeconds: 10
resources:
requests:
cpu: "250m"
memory: "1Gi"
limits:
cpu: "1000m"
memory: "2Gi"
Key deployment notes
| Aspect | Details |
|---|---|
| Custom command | Skips seed-app-store.ts which resets app enabled states on every restart |
| Init containers | wait-for-postgres ensures DB is ready; migrate runs Prisma migrations |
| Probes | TCP socket (Cal.com does not expose /api/health) |
| Environment | DATABASE_URL and DATABASE_DIRECT_URL both required (Prisma schema) |
| Hostname validation | ALLOWED_HOSTNAMES required for Cal.com v6+ (JSON array format) |
Points to shared email-relay namespace (MX validation + Graph API) | |
| Memory | NODE_OPTIONS and resource limits prevent OOM crashes |
| App configuration | Google Calendar, Zoom configured via Admin UI (settings persist across restarts) |
Cal.com v6+ requires ALLOWED_HOSTNAMES to be set. Without it, authenticated routes (/bookings, /event-types) fail with 503 errors due to hostname validation in orgDomains.ts. The value must be a JSON array: '["yourdomain.com"]'.
Services
# ClusterIP for internal access
apiVersion: v1
kind: Service
metadata:
name: calcom
namespace: calcom
spec:
selector:
app: calcom
ports:
- port: 3000
targetPort: 3000
---
# LoadBalancer for LAN admin access (optional)
apiVersion: v1
kind: Service
metadata:
name: calcom-lb
namespace: calcom
annotations:
metallb.universe.tf/loadBalancerIPs: "192.168.1.100"
spec:
type: LoadBalancer
selector:
app: calcom
ports:
- port: 3000
targetPort: 3000
---
# Headless service for PostgreSQL StatefulSet
apiVersion: v1
kind: Service
metadata:
name: calcom-db
namespace: calcom
spec:
clusterIP: None
selector:
app: calcom-db
ports:
- port: 5432
targetPort: 5432
Ingress
Ingress exposes Cal.com on the public domain using a Cloudflare Origin CA certificate.
# k8s/prod/40-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: calcom
namespace: calcom
annotations:
# Rate limiting disabled - Cloudflare provides DDoS protection
# Cal.com is a heavy SPA that makes many parallel API calls
# nginx.ingress.kubernetes.io/limit-rps: "100"
# nginx.ingress.kubernetes.io/limit-connections: "50"
spec:
ingressClassName: nginx
tls:
- hosts:
- cal.example.com
secretName: calcom-tls
rules:
- host: "cal.example.com"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: calcom
port:
number: 3000
Do not enable ingress rate limiting for Cal.com. It's a heavy SPA that makes many parallel API calls on page load, easily exceeding typical limits and causing 503 errors. Cloudflare provides DDoS protection at the edge.
Origin CA certificate
The certificate is issued by Cloudflare Origin CA for end-to-end encryption through the tunnel.
# k8s/prod/60-originissuer.yaml
apiVersion: cert-manager.k8s.cloudflare.com/v1
kind: OriginIssuer
metadata:
name: cloudflare-origin
namespace: calcom
spec:
requestType: OriginECC
auth:
serviceKeyRef:
name: cfapi-token
key: api-token
---
# k8s/prod/70-certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: calcom-tls
namespace: calcom
spec:
secretName: calcom-tls
issuerRef:
group: cert-manager.k8s.cloudflare.com
kind: OriginIssuer
name: cloudflare-origin
dnsNames:
- cal.example.com
duration: 8760h # 1 year
renewBefore: 720h # 30 days
Cloudflared ConfigMap update
Add the hostname to your existing cloudflared ConfigMap:
# In your cloudflare repo's ConfigMap
- hostname: cal.example.com
service: https://ingress-nginx-controller.ingress-nginx.svc.cluster.local:443
originRequest:
originServerName: cal.example.com
caPool: /etc/cloudflared/certs/origin_ca.pem
After updating: push to Git, reconcile Flux, then restart cloudflared:
kubectl rollout restart deployment/cloudflared -n cloudflare
Kustomization
The kustomization ties all resources together:
# k8s/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- 10-secret-db.enc.yaml
- 11-secret-calcom.enc.yaml
- 20-db-statefulset.yaml
- 30-calcom-deployment.yaml
- 40-ingress.yaml
- 50-secret-cfapi-token.enc.yaml
- 60-originissuer.yaml
- 70-certificate.yaml
Email relay is managed separately in the email-relay namespace. See Email relay for the full deployment.