Domains & HTTPS
Single-workspace (self-host)#
- Point your domain at the server: an
Arecord (orCNAME) forblog.example.com. - Set
APP_URL=https://blog.example.comin.env. - Start with TLS:
ACME_EMAIL=you@example.com docker compose --profile app --profile tls up -d. Caddy asks the app (/api/domains/check) whether a hostname is served and issues a Let's Encrypt certificate on first request. - Optionally add the hostname under Admin → Domains so it is listed and verified.
Behind an existing reverse proxy (nginx, Traefik, a PaaS): terminate TLS there and forward to port 3000 with the Host header intact.
Multi-tenant (cloud mode)#
TENANCY=multiandROOT_DOMAIN=bookly.app: every workspace is served at<slug>.bookly.app.- A workspace owner adds
blog.example.comunder Admin → Domains, publishes theTXTrecord shown at_bookly.blog.example.com, and points the host atROOT_DOMAIN(CNAME) or its IP (A). Verify checks both with DNS. - Only verified domains resolve to a workspace and only verified domains get certificates (Caddy on-demand TLS uses the same check).
- Wildcard DNS (
*.bookly.app) plus a wildcard certificate for the root domain is recommended; Caddy can do this with a DNS challenge plugin, or use your platform's wildcard support.
Platform notes#
- Vercel: add each custom domain to the project (Domains API for automation); Vercel terminates TLS. The verification flow above still gates which hosts resolve to a workspace.
- Cloudflare: proxied records work; use "Full (strict)" SSL mode.
Vercel cron#
apps/web/vercel.json runs /api/cron/publish once a day (Hobby plan limit). Scheduled posts do not depend on it: a post whose publish time has passed is public at read time and cached pages refresh within the hour. The cron only normalizes the status column. On a Pro plan you can raise the schedule to */5 * * * *.