How to Self-Host a Booking Page with Cal.com (Let Clients Pick a Slot Without the Email Tennis)

"When are you free?" "Tuesday or Thursday?" "Thursday 2?" "Can we do 3?" — six emails for one appointment. A booking page shows your free slots and lets the other person pick; the event lands in your calendar with a confirmation to both. Cal.com is the open-source Calendly, and it self-hosts: your calendar via CalDAV (Radicale, 150, or Nextcloud, 32) or Google, event types for the things you offer, working hours, buffers, and an embeddable page for the website (110). For a computer-repair business, a tutor, a consultant, it replaces a £10-a-month tool.
Quick reference
| Task | What |
|---|---|
| Folder | ~/docker/calcom/compose.yaml (app + PostgreSQL) |
| Web | https://book.example.com — public |
| Needs | NEXTAUTH_SECRET, CALENDSO_ENCRYPTION_KEY, NEXT_PUBLIC_WEBAPP_URL, SMTP |
| Calendar | Settings → Calendars → CalDAV (Radicale/Nextcloud) or Google |
| Event types | "30-min call", "Home visit (90 min)" with location, buffers, limits |
| Embed | or the embed snippet on the site |
1. The compose file
Cal.com's image is large and wants a bit of RAM (2 GB). The official docker repo provides the compose; the essentials:
mkdir -p ~/docker/calcom && cd ~/docker/calcom
nano compose.yaml
services:
calcom:
image: calcom/cal.com:latest
container_name: calcom
ports:
- "127.0.0.1:3016:3000"
environment:
NEXT_PUBLIC_WEBAPP_URL: https://book.example.com
NEXTAUTH_URL: https://book.example.com
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
CALENDSO_ENCRYPTION_KEY: ${ENCRYPTION_KEY}
DATABASE_URL: postgresql://calcom:${DB_PASSWORD}@calcom-db:5432/calcom
DATABASE_DIRECT_URL: postgresql://calcom:${DB_PASSWORD}@calcom-db:5432/calcom
NEXT_PUBLIC_LICENSE_CONSENT: agree
EMAIL_FROM: bookings@example.com
EMAIL_SERVER_HOST: smtp.gmail.com
EMAIL_SERVER_PORT: 587
EMAIL_SERVER_USER: bookings@example.com
EMAIL_SERVER_PASSWORD: ${SMTP_PASSWORD}
NEXT_PUBLIC_DISABLE_SIGNUP: "true"
depends_on: [calcom-db]
restart: unless-stopped
calcom-db:
image: postgres:16-alpine
container_name: calcom-db
environment:
POSTGRES_USER: calcom
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: calcom
volumes:
- ./db:/var/lib/postgresql/data
restart: unless-stopped
.env: two secrets (openssl rand -base64 32 each), the DB password, the SMTP app password (49). Set DISABLE_SIGNUP to "false" for the very first start so you can create your account, then back to "true".
docker compose up -d
docker compose logs -f calcom # migrations run on first start; a few minutes
2. Proxy and account
book.example.com → 127.0.0.1:3016 with the WebSocket lines (28), HTTPS — public, since clients use it. Open it, Sign up, then flip DISABLE_SIGNUP back and up -d. The onboarding asks for your name, time zone and working hours (Mon–Fri 9–5, say).
3. Connect your calendar
Settings → Calendars → Add: CalDAV with the Radicale URL (https://cal.example.com/john/) and login, or Nextcloud's CalDAV URL, or Google (needs OAuth app setup — CalDAV is simpler). Pick which calendar to check for conflicts (all of them) and which to add bookings to (your work one). Now busy times are blocked and bookings appear in your calendar on your phone (150).
4. Event types
Event types → New:
- "15-minute phone call" — location: phone number asked at booking; duration 15; buffer 5 after.
- "Remote support session" — 45 min; location: a RustDesk ID (175) or "I'll call you"; a custom question "What's the problem?"; limit 4 per day.
- "Home visit" — 90 min; location: ask attendee for address; buffer 30 before and after for travel; only Tue/Thu afternoons (availability schedule per event type); minimum notice 24 h.
Each has a URL (book.example.com/john/home-visit). Workflows add reminders (email 24 h before; SMS needs a provider). Confirmation required for the ones you want to vet. Payments per event type via Stripe if deposits make sense.
5. Put it on the website
A link from the Hugo site (110), or embed: the event type's Embed button gives an iframe or a script snippet for an inline calendar, a popup button or a floating "Book a time" tab. Email signature: the link.
6. The booking
The client picks a slot in their own time zone, fills the questions, confirms. Both get an email with an .ics; the event is in your calendar; the client gets a reschedule/cancel link; reminders fire. You get an ntfy (93) via a Cal.com webhook → n8n (77) if you want a buzz.
Back up
./db dumped nightly (65) plus .env (the encryption key is needed to read stored calendar credentials). It's small; it's also your diary.
Troubleshooting
Calendar connection fails. The CalDAV URL needs the trailing slash and HTTPS with a trusted certificate; test it in a desktop client first.
No slots shown. Working hours or the event's availability schedule; time zone mismatch between your profile and the calendar.
Emails not sent. SMTP settings; docker compose logs calcom | grep -i smtp.
Slow first load. It's a big Next.js app; give it 2 GB and an SSD. Subsequent loads are fine.
Wrapping up
Two containers behind a public HTTPS name, your CalDAV calendar connected, three event types with buffers and questions, a link in the email signature. Clients book themselves into gaps you've already blessed, the appointment is on your phone, and the email tennis is over.
