How to Self-Host Cal.com in Production (Docker + Reverse Proxy + HTTPS) — Step-by-Step
A practical, production-minded walkthrough for self-hosting Cal.com with Docker, a reverse proxy, and HTTPS. You’ll learn the recommended architecture, what to configure for security and reliability, and how to verify your deployment end-to-end.
A production-ready setup uses Docker Compose for repeatable deployments, a reverse proxy (Traefik or Nginx) for routing and security, and Let’s Encrypt for HTTPS certificates with auto-renewal. The article walks through creating an .env file, a Compose stack (Cal.com + Postgres + Traefik), and validating TLS on your domain.
You need a Linux server (Ubuntu 22.04+ is common), a domain (e.g., schedule.example.com) with DNS A/AAAA pointing to the server, Docker + Docker Compose, and ports 80 and 443 open. For small teams, the article recommends at least 2 vCPU, 4–8 GB RAM, and SSD storage.
Traefik is recommended for Docker-first setups because it auto-discovers containers via labels and includes built-in Let’s Encrypt support. Nginx is also stable and common, but typically requires manual virtual host configuration and separate certificate management (e.g., certbot).
You’ll typically set your domain/host, Postgres credentials, and application secrets like NEXTAUTH_SECRET and CALENDSO_ENCRYPTION_KEY. The article also strongly recommends configuring SMTP variables for email, and notes that exact variable names can change by Cal.com version.
The article uses Traefik with an ACME (Let’s Encrypt) certificate resolver and the HTTP-01 challenge on port 80 to issue certificates, then serves the app on HTTPS (443). Certificates are stored in a mounted volume so they persist across container restarts.
The most common causes are DNS not pointing to your server, port 80 being blocked (required for the HTTP challenge), or another service already using ports 80/443. Checking Traefik logs is the recommended first step to pinpoint the failure.
Start it with docker compose up -d and verify containers with docker compose ps. If something looks wrong, follow logs for Traefik and Cal.com using docker compose logs -f to diagnose routing, TLS, or application errors.
No—Postgres should not be exposed to the internet. The provided Compose example does not publish Postgres ports, which is the recommended approach for a safer production baseline.
The article suggests keeping Postgres unexposed, adding security headers at the proxy layer (e.g., HSTS), and using strong secrets stored securely. For serious workloads, it recommends considering managed Postgres for easier backups, patching, and recovery.
How to Self-Host Cal.com in Production (Docker + Reverse Proxy + HTTPS) — Step-by-Step
Self-hosting a scheduling platform is usually about **control**: your own domain, your own data boundaries, your own observability and uptime posture. If you’re deploying [PRODUCT_LINK]Cal.com[/PRODUCT_LINK] for a team or product, the “it runs on my laptop” Docker setup isn’t enough—you’ll want a production baseline with:
- **Dockerized services** (repeatable, versioned deployments)
- A **reverse proxy** (clean routing + security headers)
- **HTTPS** (Let’s Encrypt certificates, automatic renewal)
- Sensible **secrets management**, backups, and health checks
This guide focuses on a proven approach: **Docker Compose + reverse proxy (Traefik or Nginx) + Let’s Encrypt**, with practical steps you can adapt to your environment.
---
What “production” means for a self-hosted Cal.com setup
Before touching configs, align on what you’re optimizing:
1. **Reliability**: service restarts, health checks, persistent volumes
2. **Security**: TLS everywhere, least-privilege networking, secret hygiene
3. **Maintainability**: easy upgrades, clear separation of concerns
4. **Observability**: logs, metrics, and alerting (at least basic)
A typical production layout looks like:
- `calcom` app container
- `postgres` database container (or managed Postgres)
- optional `redis` (depending on features and scaling)
- reverse proxy container (Traefik/Nginx)
- Let’s Encrypt integration for certificates
---
Prerequisites
You’ll need:
- A Linux server (Ubuntu 22.04+ is common)
- A domain name (e.g., `schedule.example.com`)
- DNS A/AAAA record pointing to your server
- Docker + Docker Compose installed
- Ports **80** and **443** open to the internet
Minimum recommended server sizing
For small teams:
- 2 vCPU
- 4–8 GB RAM
- SSD storage
If you expect higher traffic or many team members, consider a managed Postgres and scale app containers horizontally.
---
Step 1: Prepare the host (firewall + updates + directories)
On Ubuntu:
```bash
sudo apt update && sudo apt -y upgrade
sudo apt -y install ca-certificates curl ufw
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
Create a working directory:
```bash
mkdir -p /opt/calcom
cd /opt/calcom
```
Keep configuration and persistent data **outside** containers:
```bash
mkdir -p ./data/postgres
mkdir -p ./traefik
```
---
Step 2: Choose a reverse proxy approach (Traefik vs Nginx)
You have two solid patterns:
Option A — Traefik (recommended for Docker-first)
- Auto-discovers containers via Docker labels
- Built-in Let’s Encrypt support
- Easy multi-app hosting on one server
Option B — Nginx (familiar + explicit config)
- Very common and stable
- Requires manual virtual host config + certbot (or nginx-proxy + companion)
In this guide, we’ll use **Traefik** because it’s clean and production-friendly for Compose.
---
Step 3: Create production environment variables
Create an `.env` file:
```bash
touch .env
chmod 600 .env
```
Example (adjust values):
```env
Domain
CALCOM_HOST=schedule.example.com
Database
POSTGRES_DB=calcom
POSTGRES_USER=calcom
POSTGRES_PASSWORD=change_me_long_random
App secrets
NEXTAUTH_SECRET=change_me_long_random
CALENDSO_ENCRYPTION_KEY=change_me_long_random
Email (strongly recommended for production)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=smtp-user
SMTP_PASS=smtp-password
SMTP_SECURE=false
```
**Notes:**
- Use a password manager or secret generator for secrets.
- Store secrets securely (consider Docker secrets or a vault for larger orgs).
For the exact required variables and latest names, refer to the official documentation for [PRODUCT_LINK]self-hosting Cal.com[/PRODUCT_LINK]—environment variables can evolve across versions.
---
Step 4: Create a Docker Compose file (app + Postgres + Traefik)
Create `docker-compose.yml`:
```yaml
services:
traefik:
image: traefik:v3.1
container_name: traefik
restart: unless-stopped
command:
- --api.dashboard=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik:/letsencrypt
postgres:
image: postgres:16
container_name: calcom-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
calcom:
Use the official image/tag appropriate for your deployment
See Cal.com docs for recommended images and versions.
image: calcom/cal.com:latest
container_name: calcom
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
Core URL
NEXT_PUBLIC_WEBAPP_URL: https://${CALCOM_HOST}
Database
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
Auth/secrets
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
CALENDSO_ENCRYPTION_KEY: ${CALENDSO_ENCRYPTION_KEY}
EMAIL_FROM: ${EMAIL_FROM}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_USER: ${SMTP_USER}
SMTP_PASS: ${SMTP_PASS}
SMTP_SECURE: ${SMTP_SECURE}
labels:
- traefik.enable=true
- traefik.http.routers.calcom.rule=Host(`${CALCOM_HOST}`)
- traefik.http.routers.calcom.entrypoints=websecure
- traefik.http.routers.calcom.tls=true
- traefik.http.routers.calcom.tls.certresolver=le
- traefik.http.services.calcom.loadbalancer.server.port=3000
Optional: redirect HTTP -> HTTPS
- traefik.http.routers.calcom-http.rule=Host(`${CALCOM_HOST}`)
- traefik.http.routers.calcom-http.entrypoints=web
- traefik.http.routers.calcom-http.middlewares=redirect-to-https
- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
```
Why this Compose setup is production-friendly
- **`restart: unless-stopped`** gives resilience across reboots
- Postgres has a **healthcheck**, so the app doesn’t start too early
- Traefik handles **TLS** + renewals automatically
- You can extend it with rate limiting, headers, IP allowlists, etc.
If you want an alternative to Traefik (or need to integrate with an existing Nginx), the official [PRODUCT_LINK]Cal.com Docker deployment guidance[/PRODUCT_LINK] is the best reference point.
---
Step 5: Start the stack and validate HTTPS
Run:
```bash
docker compose up -d
docker compose ps
```
Check logs if something looks off:
```bash
docker compose logs -f traefik
docker compose logs -f calcom
```
Validate
1. Visit `https://schedule.example.com`
2. Confirm:
- Certificate is valid (Let’s Encrypt)
- HTTP redirects to HTTPS
- App loads and initial setup works
If TLS issuance fails, it’s almost always:
- DNS not pointing correctly
- Port 80 blocked (Let’s Encrypt HTTP challenge needs it)
- Another service already binding 80/443
---
Step 6: Harden the deployment (quick wins)
1) Don’t expose Postgres to the internet
In the Compose above, Postgres has no published ports—good. Keep it that way.
2) Add security headers at the proxy layer
Traefik can apply headers middleware (HSTS, X-Content-Type-Options, etc.). If you’re in a regulated environment, define a headers middleware and attach it to the router.
3) Use managed Postgres for serious workloads
Running Postgres in Docker is fine for small/medium deployments, but managed Postgres simplifies:
- automated backups
- point-in-time recovery
- patching
4) Backups (non-negotiable)
At minimum, run a nightly dump:
```bash
docker exec -t calcom-postgres pg_dump -U $POSTGRES_USER $POSTGRES_DB > /opt/calcom/backups/calcom-$(date +%F).sql
```
Store backups off-host (S3, Backblaze, etc.). Test restores.
5) Pin versions
Replace `latest` with a tested version tag and upgrade intentionally. This is one of the simplest ways to keep production stable.
---
Step 7: Updates without downtime (practical approach)
A reasonable update workflow:
1. Read release notes
2. Backup database
3. Pull new images
4. Restart
```bash
docker compose pull
docker compose up -d
```
For near-zero downtime, you’d typically add:
- multiple app replicas behind the proxy
- a separate migration job/runbook
- health-gated rollouts
That’s beyond a “5-minute read” guide, but the architecture above is a good foundation.
---
Common production issues (and how to avoid them)
“I get a 502/Bad Gateway”
- App not listening on expected port (ensure `3000` in Traefik service label matches)
- Container crash-looping (check `docker compose logs -f calcom`)
“Calendar integrations fail”
- Confirm your public URL matches `NEXT_PUBLIC_WEBAPP_URL`
- Verify HTTPS is valid (some OAuth flows are strict)
- Ensure correct callback URLs in Google/Microsoft developer consoles
“Emails don’t send”
- SMTP credentials or port mismatch
- Provider requires TLS (`SMTP_SECURE=true`) or app passwords
---
Conclusion
Self-hosting Cal.com in production is mainly about treating it like any modern web app: **containerize it, put it behind a proper reverse proxy, enable HTTPS, and operationalize backups and upgrades**.
Once the basics are in place, you can iterate: add monitoring, move Postgres to a managed service, and apply stricter network/security policies. If you want to go deeper on self-hosting options and deployment patterns, the official [PRODUCT_LINK]Cal.com scheduling platform docs[/PRODUCT_LINK] are worth bookmarking.