How it works
GitHub ──webhook──▶ Cloudflare ──tunnel──▶ cloudflared ─┬─▶ berthd 127.0.0.1:7777 (hooks.<domain>)
visitor ───────────▶ edge └─▶ Caddy 127.0.0.1:7780 (*.<domain>)
LAN visitor ─────────────────────────────────────────────▶ Caddy :443, :80 → 443 (*.<mac>.local)
│
berth CLI ──unix socket──▶ berthd ──container build/run──▶ app VMs on 127.0.0.1:8001–8999
└──Caddy admin API │ berth network
▼
pg-<name>, redis-<name> VMs on the network gateway
berthdis the brain. It takes webhooks, builds withcontainer build, runs each release in its own VM, health-checks it, and switches traffic by updating a DNS SRV record that Caddy re-reads every second (so a deploy never reloads Caddy). Then it watches everything. It's the only thing that writes to~/.berth.- Caddy routes each hostname to the right app, does HTTPS on the LAN, compresses responses, writes access logs and enforces basic auth.
- cloudflared keeps an outbound connection to Cloudflare, so the Mac needs no open ports. HTTPS for public URLs ends at Cloudflare's edge.
- Every app and database is its own lightweight VM with its own Linux kernel, so a compromised app can't get at the others or at macOS.
All the state is plain files you can read:
~/.berth/
config.json domain, ports, defaults
apps/<app>/app.json repo, branch, mounts, links, domains
apps/<app>/env live config (mode 0600)
apps/<app>/releases/ one JSON file per release
apps/<app>/logs/ app.log, build-<n>.log, release-<n>.log, cron.log, access.<created>.log
apps/<app>/cron.json each scheduled job's last run
services/<kind>/<name>/ database settings
backups/ database dumps and SQLite backups
data/apps/<app>/ storage:mount folders
secrets/ tokens and tunnel credentials (mode 0700)
logs/ berthd, Caddy and cloudflared logs
offsite.json the last off-Mac backup (its settings are in secrets/)
events.log the event log
The full design, including what changed along the way, is in design.md.