Best of Product Hunt

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.

Share:

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)

[email protected]

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

- [email protected]

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

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.

More from Cal.com