Skip to Content

Production Guide: Deploy Cal.com with Docker Compose + Nginx + PostgreSQL + Redis on Ubuntu

A production-first scheduling stack with TLS, worker isolation, backups, and operational guardrails for growing teams.

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 .env with 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/.env and 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

Talk to us

If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.

Contact Us

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.

Production Guide: Deploy BookStack with Docker Compose + Traefik + MariaDB on Ubuntu
A practical production runbook for secure deployment, backups, upgrades, and verification.