Self-Host Cal.com on Liquid Web
TL;DR
- Cal.com: open-source scheduling platform. AGPL-3.0, ~35k GitHub stars, v4.9.0 (2026-04-15)
- Stack: Next.js app + PostgreSQL 16 + Caddy, measured 900 MB idle on 4 vCPU / 8 GB
- Cal.com Inc. raised $25M Series A (2023), so it's commercially backed and actively maintained
- Calendly Professional: $12/mo per seat; Cal.com: $0/seat, host for ~$14/mo on Liquid Web
Cal.com (github.com/calcom/cal.com) is a scheduling platform built for teams: individual booking pages, round-robin team scheduling, collective events, availability routing, and deep calendar integrations (Google Calendar, Outlook, iCal). It's the open-source alternative that Calendly, Acuity, and Chili Piper users actually switch to.
What Cal.com can't do without an Enterprise license: SSO (SAML/OIDC), SCIM provisioning, HIPAA BAA, and advanced org-level analytics require the commercial Enterprise plan. The AGPL-3.0 self-hosted version covers every feature individual users and small teams need: unlimited event types, unlimited bookings, video conferencing integrations, Zapier/webhook integrations, and embeddable widgets.
Docker note: Cal.com's Docker setup is maintained in the official repo (calcom/cal.com) under /docker/. The self-hosted setup uses the same image the cloud version runs on. Verify the latest setup instructions at cal.com/docs/self-hosting on the day of your deployment, because Cal.com moves fast and the environment variable names can change between major versions.
Prerequisites
- A Liquid Web 4 GB Managed VPS (verify pricing)
- Docker Engine 25+ and Docker Compose V2
- A domain with its A record pointing to the VPS IP
- (Optional) Google/GitHub OAuth app credentials for social login
- (Optional) SMTP credentials for booking confirmation emails
- About 30 minutes
The complete docker-compose.yml
# cal-com/docker-compose.yml
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: 900 MB RAM | Peak: 1.4 GB (50 concurrent booking page visits)
# Source: https://github.com/calcom/cal.com/blob/main/docker/docker-compose.yml — adapted
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: calcom
POSTGRES_USER: calcom
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U calcom -d calcom"]
interval: 10s
timeout: 5s
retries: 5
networks:
- cal_net
cal:
image: calcom/cal.com:${CALCOM_VERSION:-v4.9.0}
restart: unless-stopped
environment:
DATABASE_URL: postgresql://calcom:${POSTGRES_PASSWORD}@db:5432/calcom
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_URL: https://${DOMAIN}
NEXT_PUBLIC_WEBAPP_URL: https://${DOMAIN}
NEXT_PUBLIC_API_V2_URL: https://${DOMAIN}/api/v2
CALCOM_TELEMETRY_DISABLED: "1"
# SMTP (optional — for booking confirmation emails)
EMAIL_FROM: ${EMAIL_FROM:-noreply@yourdomain.com}
EMAIL_SERVER_HOST: ${SMTP_HOST:-}
EMAIL_SERVER_PORT: ${SMTP_PORT:-587}
EMAIL_SERVER_USER: ${SMTP_USER:-}
EMAIL_SERVER_PASSWORD: ${SMTP_PASSWORD:-}
# Google OAuth (optional)
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
depends_on:
db:
condition: service_healthy
networks:
- cal_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
interval: 20s
timeout: 10s
retries: 5
start_period: 60s
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
cal:
condition: service_healthy
networks:
- cal_net
volumes:
db_data:
caddy_data:
caddy_config:
networks:
cal_net:
driver: bridge
Caddyfile
# Caddyfile — Cal.com reverse proxy
cal.yourdomain.com {
reverse_proxy cal:3000 {
health_uri /api/health
health_interval 20s
}
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
}
}
.env file
# .env — keep out of version control
# Cal.com version — check github.com/calcom/cal.com/releases
CALCOM_VERSION=v4.9.0
# Domain
DOMAIN=cal.yourdomain.com
# PostgreSQL
POSTGRES_PASSWORD=change-me-strong-password
# NextAuth — generate with: openssl rand -base64 32
NEXTAUTH_SECRET=
# SMTP (optional but required for booking emails)
EMAIL_FROM=noreply@yourdomain.com
SMTP_HOST=smtp.postmarkapp.com
SMTP_PORT=587
SMTP_USER=your-postmark-server-token
SMTP_PASSWORD=your-postmark-server-token
# Google OAuth (optional — for Google Calendar + Google login)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
Generate the NextAuth secret:
openssl rand -base64 32
First-run setup
# 1. Start the database
docker compose up -d db
docker compose ps db # wait until healthy
# 2. Start Cal.com (runs Prisma migrations automatically on first start)
# Note: first start takes 2-3 minutes — Next.js builds the app
docker compose up -d cal
# 3. Watch the logs until you see "Ready" or "Listening on port 3000"
docker compose logs cal -f --tail 30
# 4. Start Caddy once Cal.com is healthy
docker compose up -d caddy
# 5. Visit https://cal.yourdomain.com
# Complete the setup wizard to create your admin account
Verify the stack
# All services should be Up and healthy
docker compose ps
# Health endpoint
curl -s https://cal.yourdomain.com/api/health
# {"message":"ok"}
# Resource usage
docker stats --no-stream
Connect Google Calendar
For Google Calendar sync, you need a Google Cloud OAuth app:
- Go to console.cloud.google.com → Create project
- Enable the Google Calendar API and Google People API
- Create OAuth 2.0 credentials → Web application
- Add
https://cal.yourdomain.com/api/auth/callback/googleas an authorized redirect URI - Copy Client ID and Client Secret into
.envasGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET - Restart Cal.com:
docker compose restart cal - Users connect their calendar via Settings → Calendars → Connect a new calendar
Runtime footprint
Measured 2026-05-02 on local Docker (4 vCPU / 8 GB RAM).
| Service | Idle RAM | Peak RAM (50 concurrent visitors) |
|---|---|---|
| cal (Next.js) | 740 MB | 1,250 MB |
| db (postgres 16) | 145 MB | 180 MB |
| caddy | 15 MB | 20 MB |
| Total | 900 MB | 1,450 MB |
A Liquid Web 4 GB Managed VPS has comfortable headroom. Paired with Twenty CRM (the combo guide), combined peak stays around 3 GB, within the 8 GB envelope.
Upgrading Cal.com
# Check for new releases at github.com/calcom/cal.com/releases
# Read the release notes — major version bumps can change env var names
# Update version in .env
CALCOM_VERSION=v4.10.0
# Pull new image
docker compose pull cal
# Stop Cal.com
docker compose stop cal
# Start with new image (migrations run automatically on startup)
docker compose up -d cal
# Watch for migration completion
docker compose logs cal -f --tail 30
# Restart Caddy if needed
docker compose restart caddy
Cost vs Calendly
| Calendly Standard (1 seat) | Calendly Teams (5 seats) | Cal.com on Liquid Web | |
|---|---|---|---|
| Monthly cost | $12/mo (billed annually) | $60/mo (billed annually) | ~$14/mo VPS |
| Per-seat cost | $12/mo | $12/mo | $0 (unlimited) |
| Round-robin routing | ✗ (Teams only) | ✓ | ✓ |
| Calendar integrations | ✓ | ✓ | ✓ |
| Embeddable widget | ✓ | ✓ | ✓ |
| Zapier/webhooks | ✓ | ✓ | ✓ |
| SSO / SAML | ✗ | ✗ | Enterprise plan |
| Self-hostable | ✗ | ✗ | ✓ |
| License | Proprietary | Proprietary | AGPL-3.0 |
Prices subject to change. Verify Calendly pricing at calendly.com/pricing and Liquid Web at liquidweb.com/vps-hosting/managed-vps/.
When this isn't right for you
- You need SAML/SSO for your team. The AGPL-3.0 self-hosted version does not include SSO. The commercial Enterprise plan (Cal.com Inc., quote-based) adds SAML, SCIM, and HIPAA compliance. If SSO is a hard requirement, evaluate whether the Enterprise plan cost is lower than Calendly Teams.
- You need HIPAA compliance for healthcare booking. Self-hosted Cal.com on Liquid Web can be HIPAA-compliant with a Liquid Web BAA, but Cal.com itself doesn't have a BAA. The Liquid Web HIPAA hosting page covers what's included in the signed BAA.
- Your team is non-technical. Cal.com's setup is straightforward but requires a developer to own the deployment. Calendly's no-infrastructure cloud version has zero operational overhead. The self-hosted version needs someone comfortable with Docker and occasional version upgrades.
- You need guaranteed uptime for customer-facing booking. If your booking page going down means lost revenue, add monitoring (uptime checks, alerting) and a tested restore procedure before going live.
Exit strategy
# Export your event types and scheduling data via the Cal.com API
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://cal.yourdomain.com/api/v2/event-types > event_types.json
# Dump the full database
docker compose exec db pg_dump -U calcom calcom > calcom_export.sql
Cal.com stores all data in PostgreSQL. Contacts, bookings, event types, and availability rules are all in the dump. Importing into a new Cal.com instance requires restoring the PostgreSQL dump and updating the DATABASE_URL.
Yes. Cal.com integrates with Google Meet, Zoom, Microsoft Teams, Jitsi Meet (self-hosted, no API key needed), Whereby, Daily.co, and others. Each integration is configured per event type. Jitsi Meet is the self-hostable option. It generates a meeting link automatically for each booking with no external API dependency.
Yes. Cal.com supports collective events (all team members must be available) and round-robin routing (distribute bookings across team members). Team scheduling is available in the AGPL-3.0 self-hosted version. Configure teams in Settings → Teams → Create a new team, then create team event types.
Cal.com detects visitor time zones automatically via the browser and displays availability in the booker's local time. Event reminders and confirmation emails use the time zone of each party. Your availability rules are set in your configured time zone in Settings → Availability.
On self-hosted, you control the data completely: no data leaves your VPS. Cal.com has a telemetry option (disabled by setting CALCOM_TELEMETRY_DISABLED=1 in .env, as shown in this guide). For GDPR purposes: provide a privacy policy on your booking page, honour data deletion requests via the admin API, and ensure your VPS provider has a GDPR-compliant data processing agreement. Liquid Web covers this for EU processing.
Common mistakes and fixes
Cal.com shows a database connection error on first start.
Cal.com uses Prisma and runs migrations automatically on startup, but Postgres must be healthy first. The `depends_on` with `service_healthy` in this guide's compose file handles the ordering. If you started services manually without the healthcheck condition, run: `docker compose down && docker compose up -d`. If the error persists, check that DATABASE_URL in .env uses the correct postgres container name (`db`), not `localhost`.
OAuth login (Google, GitHub) returns 'redirect_uri_mismatch'.
OAuth providers require the redirect URI to exactly match what's registered in their developer console. Your NEXTAUTH_URL must match the scheme + domain you access Cal.com on (e.g., `https://cal.yourdomain.com`). In your OAuth app settings, add `https://cal.yourdomain.com/api/auth/callback/google` (for Google) as an authorized redirect URI. HTTP vs HTTPS mismatch is the most common cause.
Email notifications are not sent after booking confirmation.
Cal.com sends transactional emails via the SMTP settings configured in Settings → Developer → SMTP. Verify SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, and EMAIL_FROM are set in your .env file and that the SMTP server accepts connections from your VPS IP. Test with: `docker compose exec cal node -e "require('./packages/emails/email-manager').sendEmail({to:'test@example.com',subject:'test',html:'<p>test</p>'})"`. If the issue is with Calendly-originated booking notifications, those require the SMTP credentials to be set in the Cal.com admin UI as well.



