Best of Product Hunt

How to Self-Host Cal.com in Production (Docker + Reverse Proxy + SSL) — Step-by-Step

A practical, production-ready guide to self-hosting Cal.com with Docker, a reverse proxy, and HTTPS. Learn recommended architecture, environment variables, database setup, persistent storage, and operational best practices (backups, updates, and monitoring).

Share:

Run Cal.com and PostgreSQL with Docker Compose, and put a reverse proxy (like Caddy, Traefik, or Nginx Proxy Manager) in front to handle ports 80/443 and SSL/TLS. The app and database should stay on internal Docker networks, with persistent volumes and secrets stored in a .env file.

For production, use PostgreSQL as the persistent database and avoid SQLite. The guide sets up a Postgres container with a persistent volume and connects Cal.com via a DATABASE_URL.

At minimum you need NEXT_PUBLIC_WEBAPP_URL, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, DATABASE_URL, NEXTAUTH_SECRET, and CALENDSO_ENCRYPTION_KEY. The article also recommends configuring SMTP variables for reliable email in production.

In a production setup, the reverse proxy should be the only service bound to 80/443 so it can terminate TLS and route traffic. Cal.com and PostgreSQL remain on internal Docker networks to reduce exposure and simplify security.

Run Caddy in its own Docker Compose stack, publish ports 80 and 443, and connect it to the shared external Docker network (e.g., proxy). In the Caddyfile, point reverse_proxy to calcom-app:3000, and Caddy will automatically request and renew certificates once DNS is correct.

Use two networks: an internal network for app-to-database traffic and an external proxy network shared with the reverse proxy container. Create the external network once (docker network create proxy) and attach both the Cal.com service and the proxy to it.

Common causes are using the wrong upstream port (Cal.com should be reached at port 3000 in the container), missing shared proxy networking, or the app not being ready on first boot. Check container logs and verify both services are on the same proxy network.

Using 'latest' is convenient but risky; the guide recommends pinning a specific version tag (e.g., calcom/cal.com:vX.Y.Z) for controlled upgrades. Upgrade intentionally by reading release notes and rolling forward in a planned way.

A simple approach is running pg_dump inside the Postgres container and writing the SQL dump to a backup directory on the host. The guide emphasizes automating backups, storing them offsite, and most importantly testing restores on a staging instance.

The guide suggests a Linux server (commonly Ubuntu 22.04+), a domain pointing to the server, and Docker/Compose installed. For a small team, it recommends around 2 vCPU, 4–8 GB RAM, and SSD storage.

How to Self-Host [PRODUCT_LINK]Cal.com[/PRODUCT_LINK] in Production (Docker + Reverse Proxy + SSL) — Step-by-Step

Self-hosting a scheduling platform in production is less about “getting it running” and more about **running it reliably**: persistent data, secure HTTPS, sane networking, upgrades, and backups.

This guide walks you through a proven production setup for **Cal.com self-hosting with Docker**, fronted by a **reverse proxy** and **SSL/TLS**. It’s designed for teams and developers who want control over their stack without turning deployment into a science project.

---

What “production-ready” means for a Docker deployment

Before the steps, here’s the baseline we’re aiming for:

- **HTTPS by default** (Let’s Encrypt or an equivalent)

- **Reverse proxy** handling TLS termination and routing

- **PostgreSQL** as the persistent database (don’t use SQLite in production)

- **Persistent volumes** for database data (and any app uploads if applicable)

- **Environment variables** stored safely (not hard-coded into images)

- **Backups + a restore plan**

- **Upgrade strategy** (pin versions, staged rollouts)

If you follow the steps below, you’ll end up with a setup that’s easy to operate and straightforward to evolve.

---

Prerequisites

You’ll need:

- A Linux server (Ubuntu 22.04+ is common)

- A domain name (e.g., `cal.example.com`) pointed to your server

- Docker + Docker Compose installed

- Basic familiarity with DNS, SSH, and editing files

Recommended minimums for a small team:

- 2 vCPU, 4–8 GB RAM

- SSD storage

---

Architecture overview

We’ll run three core services:

1. **Reverse proxy** (Caddy, Traefik, Nginx, or Nginx Proxy Manager)

2. **Cal.com app container**

3. **PostgreSQL database**

In production, the reverse proxy should be the **only** service bound to ports 80/443. Everything else stays on an internal Docker network.

---

Step 1) Create a project directory

SSH into your server and create a working directory:

```bash

mkdir -p /opt/calcom

cd /opt/calcom

```

Create subfolders for persistence:

```bash

mkdir -p postgres-data

```

---

Step 2) Create your environment file

Create a `.env` file:

```bash

touch .env

nano .env

```

Example (adjust values to match your environment):

```env

Public URL of your Cal.com instance

NEXT_PUBLIC_WEBAPP_URL=https://cal.example.com

Database

POSTGRES_USER=calcom

POSTGRES_PASSWORD=use-a-long-random-password

POSTGRES_DB=calcom

DATABASE_URL=postgresql://calcom:use-a-long-random-password@postgres:5432/calcom

App secrets (generate strong random values)

NEXTAUTH_SECRET=generate-a-long-random-secret

CALENDSO_ENCRYPTION_KEY=generate-32+chars-random

Optional: email (recommended for production)

[email protected]

SMTP_HOST=smtp.yourprovider.com

SMTP_PORT=587

SMTP_USER=...

SMTP_PASSWORD=...

SMTP_SECURE=false

```

**Notes:**

- `NEXT_PUBLIC_WEBAPP_URL` must match your public HTTPS URL.

- Keep secrets out of git. Treat `.env` as sensitive.

If you want the canonical list of variables and up-to-date options, refer to the official docs for the self-hosted scheduler platform at [PRODUCT_LINK]Cal.com[/PRODUCT_LINK].

---

Step 3) Write a production Docker Compose file

Create `docker-compose.yml`:

```bash

nano docker-compose.yml

```

Below is a solid baseline that works well behind a reverse proxy. (You’ll adapt labels/ports depending on which proxy you choose.)

```yaml

services:

postgres:

image: postgres:16

container_name: calcom-postgres

restart: unless-stopped

environment:

POSTGRES_USER: ${POSTGRES_USER}

POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

POSTGRES_DB: ${POSTGRES_DB}

volumes:

- ./postgres-data:/var/lib/postgresql/data

networks:

- internal

calcom:

image: calcom/cal.com:latest

container_name: calcom-app

restart: unless-stopped

depends_on:

- postgres

env_file:

- .env

networks:

- internal

- proxy

networks:

internal:

driver: bridge

proxy:

external: true

```

**Why two networks?**

- `internal` isolates your database from the proxy.

- `proxy` is an external network shared with your reverse proxy container.

Create the external proxy network once:

```bash

docker network create proxy

```

---

Step 4) Choose a reverse proxy + SSL approach

You have a few production-friendly options:

Option A: Caddy (simplest HTTPS)

Caddy is popular because it can automatically obtain/renew Let’s Encrypt certificates with minimal config.

Option B: Traefik (great for Docker-native routing)

Traefik is excellent if you host multiple apps and want dynamic routing via container labels.

Option C: Nginx Proxy Manager (GUI-driven)

NPM is friendly if you prefer a web UI to manage hosts and SSL.

All three are viable. Below is a **Caddy** example because it’s concise and reliable.

---

Step 5) Run Caddy as your reverse proxy (with automatic SSL)

Create a folder:

```bash

mkdir -p /opt/caddy

cd /opt/caddy

```

Create a `docker-compose.yml` for Caddy:

```yaml

services:

caddy:

image: caddy:2

container_name: caddy

restart: unless-stopped

ports:

- "80:80"

- "443:443"

volumes:

- ./Caddyfile:/etc/caddy/Caddyfile

- caddy_data:/data

- caddy_config:/config

networks:

- proxy

volumes:

caddy_data:

caddy_config:

networks:

proxy:

external: true

```

Create the `Caddyfile`:

```bash

nano /opt/caddy/Caddyfile

```

Example:

```caddyfile

cal.example.com {

reverse_proxy calcom-app:3000

Optional hardening headers

header {

Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

X-Content-Type-Options "nosniff"

X-Frame-Options "SAMEORIGIN"

Referrer-Policy "strict-origin-when-cross-origin"

}

}

```

Bring up Caddy:

```bash

cd /opt/caddy

docker compose up -d

```

Caddy will request certificates automatically once DNS is correct.

---

Step 6) Start Cal.com

Now launch the application stack:

```bash

cd /opt/calcom

docker compose up -d

```

Check logs:

```bash

docker logs -f calcom-app

```

At this point you should be able to visit:

- `https://cal.example.com`

If you get a 502/Bad Gateway from the proxy, it’s typically:

- wrong upstream port (ensure the app listens on `3000` in the container)

- containers not on the same `proxy` network

- app not healthy yet (give it a minute on first boot)

---

Step 7) Production checklist (the stuff that prevents 2 a.m. incidents)

1) Pin versions for controlled upgrades

Using `latest` is convenient but risky in production. Prefer a pinned tag once you’re stable.

Example:

```yaml

image: calcom/cal.com:vX.Y.Z

```

When you’re ready to upgrade, read release notes and roll forward intentionally. If you want a stable reference for deployment methods, the official Docker documentation for [PRODUCT_LINK]Cal.com self-hosting[/PRODUCT_LINK] is the right place to cross-check.

2) Back up Postgres (and test restores)

A simple starting point:

```bash

docker exec -t calcom-postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > /opt/backups/calcom_$(date +%F).sql

```

Better options include:

- automated cron jobs

- offsite storage (S3-compatible, rsync to another host)

- retention policies

**Most important:** do a restore test on a staging instance.

3) Set up monitoring

At minimum:

- container restarts

- disk usage

- database size

- HTTP uptime checks

If you’re already running Prometheus/Grafana, add exporters. If not, even lightweight health checks are worth it.

4) Email deliverability

In production, configure SMTP early. Password resets and invite flows depend on email reliability.

5) Security basics

- Firewall: only expose 80/443 (and SSH)

- Keep Docker and host patched

- Use strong secrets

- Restrict database access to internal networks only

---

Common pitfalls (and how to avoid them)

“My base URL is wrong”

If redirects or callback URLs look off, verify:

- `NEXT_PUBLIC_WEBAPP_URL` uses **https** and your exact domain

“Reverse proxy works but login/callback fails”

This usually points to:

- mismatch between external URL and internal config

- missing trusted proxy headers (rare with Caddy default reverse_proxy)

“Database keeps re-initializing”

Make sure Postgres has a **persistent volume** (e.g., `./postgres-data:/var/lib/postgresql/data`). Without it, container recreation wipes the DB.

---

Conclusion

A production-grade self-hosted setup is totally achievable with a small, well-structured Docker deployment: **PostgreSQL for persistence**, **a reverse proxy for clean routing**, and **SSL for security**.

Once your instance is stable, invest in the operational basics—version pinning, backups, and monitoring—so your scheduling stays dependable.

If you want to explore deeper customization (APIs, white-labeling, multi-team configurations), the open-source scheduling platform at [PRODUCT_LINK]Cal.com[/PRODUCT_LINK] is built to support those production use cases without fighting your infrastructure.

More from Cal.com