Cal.com is an open-source scheduling platform. This guide runs its official image with PostgreSQL in Docker Compose behind Caddy, with HTTPS, mail delivery and backups.
Prerequisites
- RamNode KVM VPS running Ubuntu 24.04 LTS (26.04 LTS also works), at least 2 GB RAM and 40 GB disk; 4 GB RAM and 2 vCPUs are more comfortable during startup.
- Domain such as
cal.example.com, with an A record (and AAAA if using IPv6) pointing at the VPS. - Root SSH access initially and SMTP relay credentials for booking confirmations and resets.
- Optional Google Cloud or Microsoft Entra app credentials for calendar sync.
Replace domains and example secrets throughout. See DNS and Docker Compose.
Prepare the server
Run as root, then verify deploy can log in with SSH in a second terminal before disabling root and password access:
apt update && apt -y full-upgrade
apt install -y rsync ufw unattended-upgrades
timedatectl set-timezone UTC
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deployAfter testing the new login, run as root:
cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'CONFIG'
PermitRootLogin no
PasswordAuthentication no
CONFIG
sshd -t && systemctl restart ssh
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
dpkg-reconfigure -plow unattended-upgradesAllow your actual SSH port before enabling UFW if it differs from 22. Docker can bypass UFW for published ports; bind Cal.com only to localhost. See cloud firewall.
Install Docker Engine and Compose
As deploy:
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] 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 usermod -aG docker deploy
sudo tee /etc/docker/daemon.json > /dev/null <<'JSON'
{"log-driver":"json-file","log-opts":{"max-size":"10m","max-file":"3"}}
JSON
sudo systemctl restart dockerLog out and back in, then check docker run --rm hello-world and docker compose version.
Deploy Cal.com with Docker Compose
sudo mkdir -p /opt/calcom && sudo chown deploy:deploy /opt/calcom
cd /opt/calcom
openssl rand -hex 24 # PostgreSQL password
openssl rand -base64 32 # NEXTAUTH_SECRET
openssl rand -base64 24 # CALENDSO_ENCRYPTION_KEYCreate /opt/calcom/.env, substituting your own values:
DOMAIN=cal.example.com
POSTGRES_PASSWORD=change-me-db
NEXTAUTH_SECRET=change-me-nextauth
CALENDSO_ENCRYPTION_KEY=change-me-encryption
EMAIL_FROM=cal@example.com
EMAIL_SERVER_HOST=smtp.example.com
EMAIL_SERVER_PORT=587
EMAIL_SERVER_USER=smtp-user
EMAIL_SERVER_PASSWORD=smtp-passwordchmod 600 /opt/calcom/.envKeep the encryption key securely: losing it makes stored calendar credentials unreadable. Create /opt/calcom/compose.yml:
services:
calcom-db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: calcom
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: calcom
volumes:
- calcom-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U calcom -d calcom"]
interval: 10s
timeout: 5s
retries: 5
calcom:
image: calcom/cal.com:latest
restart: unless-stopped
environment:
DATABASE_URL: postgresql://calcom:${POSTGRES_PASSWORD}@calcom-db:5432/calcom
DATABASE_DIRECT_URL: postgresql://calcom:${POSTGRES_PASSWORD}@calcom-db:5432/calcom
NEXT_PUBLIC_WEBAPP_URL: https://${DOMAIN}
NEXTAUTH_URL: https://${DOMAIN}/api/auth
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
CALENDSO_ENCRYPTION_KEY: ${CALENDSO_ENCRYPTION_KEY}
EMAIL_FROM: ${EMAIL_FROM}
EMAIL_SERVER_HOST: ${EMAIL_SERVER_HOST}
EMAIL_SERVER_PORT: ${EMAIL_SERVER_PORT}
EMAIL_SERVER_USER: ${EMAIL_SERVER_USER}
EMAIL_SERVER_PASSWORD: ${EMAIL_SERVER_PASSWORD}
CALCOM_TELEMETRY_DISABLE: "1"
NODE_ENV: production
depends_on:
calcom-db:
condition: service_healthy
ports:
- "127.0.0.1:3000:3000"
volumes:
calcom-db:cd /opt/calcom
docker compose config --quiet
docker compose up -d
docker compose logs -f calcomFirst startup runs migrations and can take several minutes. Wait for the ready message on port 3000. After testing, pin the image to a release tag rather than latest.
Configure Caddy and HTTPS
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddySet /etc/caddy/Caddyfile to:
cal.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:3000
}sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyCaddy obtains a certificate once DNS points to the server and port 80 is accessible. Check journalctl -u caddy -f if it does not.
First-run setup
Open https://cal.example.com and follow the setup wizard at /auth/setup. Create the first admin account and choose the license option appropriate for your use (the AGPLv3 community edition needs no key). Enable two-factor authentication in Settings > Security. Test SMTP with a password reset for your own account.
For Google Calendar or Outlook integration, configure the corresponding app credentials under Settings > Admin > Apps. Register these exact HTTPS redirect URIs with the provider:
- Google:
https://cal.example.com/api/integrations/googlecalendar/callback - Microsoft:
https://cal.example.com/api/integrations/office365calendar/callback
Connect a calendar under Settings > Calendars, set availability, create an event type and book a test slot from a private browser window. If only your team needs accounts, add NEXT_PUBLIC_DISABLE_SIGNUP: "true" to the Cal.com environment, recreate with docker compose up -d and verify sign-up is disabled; some versions read this setting only at build time.
Backups and restore
Back up PostgreSQL plus .env (especially CALENDSO_ENCRYPTION_KEY) and compose.yml. Create /opt/calcom/backup.sh:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/calcom
TS=$(date +%F-%H%M)
DEST=/var/backups/calcom
mkdir -p "$DEST"
docker compose exec -T calcom-db pg_dump -U calcom -Fc calcom > "$DEST/db-$TS.dump"
cp .env "$DEST/env-$TS"
cp compose.yml "$DEST/compose-$TS.yml"
chmod 600 "$DEST"/*
find "$DEST" -type f -mtime +14 -deletesudo chmod 700 /opt/calcom/backup.sh
sudo /opt/calcom/backup.sh
sudo crontab -e
# Add: 30 3 * * * /opt/calcom/backup.shCopy backups off the VPS. See backups and object storage. To restore, put the saved .env and compose.yml in /opt/calcom, then:
cd /opt/calcom
docker compose up -d calcom-db
docker compose exec -T calcom-db pg_restore -U calcom -d calcom --clean --if-exists < /var/backups/calcom/db-YYYY-MM-DD-HHMM.dump
docker compose up -dUpdating
Cal.com runs migrations at startup; back up and read release notes before changing versions.
cd /opt/calcom
sudo ./backup.sh
docker compose pull
docker compose up -d
docker compose logs -f calcomFor rollback, restore the earlier image tag and its pre-update database dump. Upgrade PostgreSQL major versions separately with a dump and restore.
Troubleshooting
| Symptom | Check |
|---|---|
| Caddy cannot get a certificate | Confirm dig +short cal.example.com, ports 80/443 and journalctl -u caddy -f. |
| 502 response | Migrations may still be running. Check docker compose logs -f calcom, free -m and kernel OOM logs; move to 4 GB if needed. |
| Links use localhost | Set NEXT_PUBLIC_WEBAPP_URL and NEXTAUTH_URL to HTTPS and docker compose up -d --force-recreate calcom. |
| Login loop | NEXTAUTH_URL must match https://cal.example.com/api/auth. |
| Calendar OAuth redirect fails | Register the exact HTTPS callback URI above. |
| Booking mail fails | Verify authenticated SMTP relay settings and port 587; see outgoing SMTP policy. |
| Restored calendars cannot connect | Restore the original CALENDSO_ENCRYPTION_KEY or reconnect calendars. |
