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