Skip to main content

Manifests

info

This page covers the app repo structure and key manifest configurations for the Cal.com scheduling stack.

Meeting scheduling series

  1. Meeting scheduling
  2. Architecture
  3. Manifests - You are here
  4. Flux integration
  5. 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...']
warning

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)
note

Google Calendar and Zoom credentials are NOT stored in Kubernetes secrets. They are configured through the Cal.com admin UI (SettingsAdminApps) after deployment. The custom startup command (see deployment section) ensures these settings persist across pod restarts.

tip

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:

PatternValueReason
securityContext.fsGroup999PostgreSQL user GID for NFS file permissions
Probe command-h localhostAvoids Service resolution deadlock (probe resolves Service name which has no endpoints until pod is Ready)
startupProbe.failureThreshold305 minute timeout for crash recovery on slow NFS
timeoutSeconds5Tolerance for NFS latency
livenessProbe.failureThreshold6Prevents 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

AspectDetails
Custom commandSkips seed-app-store.ts which resets app enabled states on every restart
Init containerswait-for-postgres ensures DB is ready; migrate runs Prisma migrations
ProbesTCP socket (Cal.com does not expose /api/health)
EnvironmentDATABASE_URL and DATABASE_DIRECT_URL both required (Prisma schema)
Hostname validationALLOWED_HOSTNAMES required for Cal.com v6+ (JSON array format)
EmailPoints to shared email-relay namespace (MX validation + Graph API)
MemoryNODE_OPTIONS and resource limits prevent OOM crashes
App configurationGoogle Calendar, Zoom configured via Admin UI (settings persist across restarts)
warning

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
warning

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.