Scheduling is one of those systems that looks easy in staging and painful in production. Teams adopt a booking platform to eliminate email back-and-forth, but once real customer traffic arrives, the requirements change quickly: SSO boundaries matter, queues matter, webhook reliability matters, and uptime becomes tied directly to revenue and support load.
This guide shows a production-ready Cal.com deployment on Ubuntu using Docker Compose, Nginx, PostgreSQL, and Redis. It is written for operators who need deterministic upgrades, controlled secrets handling, and practical recovery steps—not just a quick demo stack. You will deploy Cal.com behind TLS, harden service boundaries, configure background workers, verify end-to-end booking flows, and establish backup/restore confidence before go-live.
Real-world use case: a services business with multiple consultants needs branded booking pages, round-robin assignment, calendar sync, and webhook notifications into CRM/automation systems. In that environment, missed jobs or broken callbacks are not minor bugs; they are missed appointments and lost revenue. The deployment pattern below prioritizes reliability and operational clarity from day one.
Architecture and flow overview
The stack is intentionally split by responsibility:
- Nginx terminates TLS, enforces sane headers, and routes traffic to Cal.com.
- Cal.com web serves UI and API.
- Cal.com worker handles async jobs (notifications, workflow tasks, queue-backed operations).
- PostgreSQL stores application and scheduling state.
- Redis provides queue/cache primitives required for responsive background processing.
Operationally, this separation gives you cleaner failure domains. If worker throughput drops, booking pages can still render while queue lag alerts fire. If the web tier needs a rolling restart, DB and Redis remain stable. This makes incident response much faster than monolithic single-container setups.
Data flow at a high level: a user opens a booking page → Cal.com validates availability and rules from PostgreSQL → booking requests enqueue follow-up tasks in Redis-backed workers → notifications/webhooks dispatch asynchronously. This architecture preserves user-facing responsiveness while keeping side effects durable and observable.
Prerequisites
- Ubuntu 22.04/24.04 host with at least 4 vCPU, 8 GB RAM, and 60+ GB disk for production baseline.
- A DNS record (for example,
calendar.yourdomain.com) pointing to your server IP. - Root or sudo access and outbound internet connectivity for image pulls and package updates.
- Docker Engine + Docker Compose plugin installed.
- SMTP credentials for transactional email delivery (booking confirmations, reminders, notifications).
- A secrets management approach (at minimum, protected
.envwith strict file permissions).
Before deployment, align on maintenance windows and define an owner for database backups. Calendar infrastructure often gets deployed by engineering, but recovery ownership is left ambiguous until the first incident. Decide that up front.
Step-by-step deployment
1) Prepare host and runtime
Patch the host first. Then install Docker components and enable service startup. Keep package drift low before introducing application complexity.
sudo apt update && sudo apt -y upgrade
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker
If the copy button does not work in your browser, manually select the code block and copy it.
2) Create project layout
Keep config, persistent data, and runtime artifacts separated. This reduces restore complexity and avoids accidental secret leaks in source control snapshots.
sudo mkdir -p /opt/calcom/{nginx,postgres,redis,backups}
cd /opt/calcom
sudo touch .env docker-compose.yml
sudo chown -R $USER:$USER /opt/calcom
If the copy button does not work in your browser, manually select the code block and copy it.
3) Define environment variables
Generate strong secrets and avoid reusing credentials between services. Treat SMTP/API tokens as production credentials with rotation plans.
# /opt/calcom/.env
POSTGRES_USER=calcom
POSTGRES_PASSWORD=CHANGE_ME_STRONG_DB_PASSWORD
POSTGRES_DB=calcom
REDIS_URL=redis://redis:6379
DATABASE_URL=postgresql://calcom:CHANGE_ME_STRONG_DB_PASSWORD@postgres:5432/calcom
NEXTAUTH_SECRET=CHANGE_ME_64PLUS_RANDOM_CHARS
CALENDSO_ENCRYPTION_KEY=CHANGE_ME_32PLUS_RANDOM_CHARS
NEXT_PUBLIC_WEBAPP_URL=https://calendar.yourdomain.com
# Email (example SMTP)
[email protected]
EMAIL_SERVER_HOST=smtp.yourprovider.com
EMAIL_SERVER_PORT=587
EMAIL_SERVER_USER=smtp-user
EMAIL_SERVER_PASSWORD=smtp-password
# Optional analytics/telemetry controls depending on your policy
CALCOM_TELEMETRY_ENABLED=false
If the copy button does not work in your browser, manually select the code block and copy it.
4) Compose stack for web, worker, DB, and Redis
Pin image tags intentionally in production. Avoid latest to keep upgrades explicit and testable.
# /opt/calcom/docker-compose.yml
services:
postgres:
image: postgres:16
container_name: calcom-postgres
restart: unless-stopped
env_file: .env
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
container_name: calcom-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- ./redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
calcom-web:
image: calcom/cal.com:v4.8.0
container_name: calcom-web
restart: unless-stopped
env_file: .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
command: ["sh", "-c", "yarn prisma migrate deploy && yarn start"]
calcom-worker:
image: calcom/cal.com:v4.8.0
container_name: calcom-worker
restart: unless-stopped
env_file: .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
command: ["yarn", "start:worker"]
nginx:
image: nginx:1.27-alpine
container_name: calcom-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
- /var/www/certbot:/var/www/certbot:ro
depends_on:
- calcom-web
If the copy button does not work in your browser, manually select the code block and copy it.
5) Configure Nginx reverse proxy
Set conservative headers and keep request handling explicit. This reduces subtle proxy behavior during peak traffic and webhook callbacks.
# /opt/calcom/nginx/default.conf
server {
listen 80;
server_name calendar.yourdomain.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name calendar.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/calendar.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/calendar.yourdomain.com/privkey.pem;
client_max_body_size 25m;
proxy_read_timeout 120s;
location / {
proxy_pass http://calcom-web:3000;
proxy_http_version 1.1;
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 https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
If the copy button does not work in your browser, manually select the code block and copy it.
6) Start services and apply first migration
Bring up data services first, then application tiers. Always inspect logs before exposing the endpoint to users.
cd /opt/calcom
docker compose pull
docker compose up -d postgres redis
sleep 8
docker compose up -d calcom-web calcom-worker nginx
docker compose ps
docker compose logs --tail=120 calcom-web calcom-worker
If the copy button does not work in your browser, manually select the code block and copy it.
7) Enable certificate issuance and renewal
If you use Certbot, complete issuance before inviting users. Expired or mismatched certs are one of the most common avoidable launch failures.
sudo apt install -y certbot
sudo certbot certonly --webroot -w /var/www/certbot -d calendar.yourdomain.com
# renewals should be automated; verify timer/cron on your host
sudo certbot renew --dry-run
If the copy button does not work in your browser, manually select the code block and copy it.
Configuration and secrets handling best practices
Production posture depends less on one perfect setting and more on repeatable controls. Start with strict file permissions:
chmod 600 /opt/calcom/.envand root-owned backup destinations.- No secrets in shell history, tickets, or chat paste logs.
- Environment parity between web and worker containers to avoid asymmetric failures.
For key management, rotate database and SMTP credentials on a schedule and after any team access change. If your org supports a secret manager (Vault, 1Password Secrets Automation, SOPS, cloud KMS integrations), move off plaintext env files once the initial deployment stabilizes.
Backups should include PostgreSQL dumps plus persistent volume snapshots where feasible. Test restore, not just backup creation. A backup you never restored is unverified inventory, not resilience. For Redis, decide whether queue state is critical in your workflow; for many teams, persistence is useful for shorter outage recovery windows but not a substitute for idempotent job design.
Keep an upgrade runbook with exact image tags, migration notes, rollback checkpoints, and post-upgrade verification steps. Treat schema migrations as change events requiring explicit sign-off in production windows.
Verification checklist
- Public URL returns 200 over HTTPS with valid certificate chain.
- Admin login works and timezone settings are correct.
- Create a test event type and complete a booking from an incognito browser.
- Booking confirmation email is delivered within expected latency.
- Webhook test payload reaches downstream endpoint and returns success.
- Worker logs show job completion without retry storms.
- PostgreSQL health checks remain passing under synthetic traffic.
cd /opt/calcom
curl -I https://calendar.yourdomain.com
docker compose ps
docker compose logs --tail=80 calcom-web
docker compose logs --tail=80 calcom-worker
docker exec -it calcom-postgres psql -U calcom -d calcom -c "select now();"
docker exec -it calcom-redis redis-cli ping
If the copy button does not work in your browser, manually select the code block and copy it.
Common issues and fixes
Issue: OAuth/calendar sync appears connected but bookings fail
Cause: provider callback URL mismatch or stale app credentials.
Fix: re-validate callback domains, rotate provider secrets, and re-authorize the affected integration account.
Issue: Worker queue grows and reminder emails are delayed
Cause: worker crash loop, Redis connectivity drift, or SMTP throttling.
Fix: inspect worker logs first, confirm Redis health, then check SMTP rate/relay restrictions and retry policy.
Issue: Random 502/504 responses under moderate traffic
Cause: proxy timeouts too low, container CPU throttling, or DB saturation.
Fix: tune Nginx timeout values, raise resource limits, review slow SQL queries, and add connection pooling if needed.
Issue: Migrations succeed in staging but fail in production
Cause: schema drift or out-of-order upgrades.
Fix: pin image tags, snapshot DB before migration, and follow a version-by-version upgrade path with rollback points.
Issue: Bookings save but webhooks intermittently time out
Cause: downstream endpoint latency or TLS handshake issues.
Fix: add retries with idempotency keys, instrument webhook latency, and validate downstream cert chain and firewall rules.
FAQ
Can I run Cal.com with only one container in production?
You can, but it is not recommended for teams with real booking volume. Splitting web and worker services improves reliability and keeps queue workload from impacting interactive request latency.
Do I need Redis if PostgreSQL is already present?
Yes for this deployment model. Redis backs queue/cache behavior that keeps async jobs responsive. Removing it usually shifts failure from clear queue semantics to less predictable latency and retries.
How should I handle backups and restores?
Automate nightly PostgreSQL dumps, retain multiple restore points, and run scheduled restore drills to a staging environment. Verify application login and booking creation after restore, not only DB import success.
What is the safest way to upgrade versions?
Pin current tags, test the target version in staging with production-like data, capture migration logs, schedule a maintenance window, snapshot DB, then roll forward with explicit verification steps and rollback criteria.
How many workers should I run?
Start with one dedicated worker and monitor queue lag plus job duration. Increase worker replicas when latency grows or reminders/webhooks become delayed during peak booking windows.
Can this stack support multi-team or multi-brand scheduling?
Yes, but enforce governance early: naming conventions, ownership boundaries, integration credentials per business unit, and webhook routing standards to avoid cross-team confusion.
What should I monitor first?
At minimum: HTTP 5xx rate, p95 latency, worker queue depth, job failure ratio, DB connection saturation, and SMTP/webhook delivery errors. These indicators catch most production regressions early.
Related internal guides
- Production Guide: Deploy BookStack with Docker Compose + Traefik + MariaDB on Ubuntu
- Production Guide: Deploy NocoDB with Docker Compose + Traefik + PostgreSQL on Ubuntu
- Production Guide: Deploy Miniflux with Docker Compose, Nginx, and PostgreSQL on Ubuntu
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.
Operational note: run periodic game-day exercises for this stack. Simulate worker outages, SMTP failures, and webhook endpoint latency spikes to validate alerting thresholds and escalation paths. Teams that rehearse these scenarios recover faster and reduce customer-facing impact during real incidents.
Capacity note: booking traffic is often bursty around local business hours and campaign launches. Profile container CPU/memory during those windows and leave headroom for background jobs. Scaling only after saturation usually means delayed reminders and support ticket spikes.
Security note: if your organization must meet strict compliance requirements, add audit trail retention policies, hardened network boundaries (host firewall and private DB network), and quarterly credential rotation checks to your standard operating procedure.