From 1be611214852c7911cda58754420f984e3b4bb41 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Sun, 5 Jul 2026 22:33:23 -0400 Subject: [PATCH] =?UTF-8?q?docs(deploy):=20quickstart-docker.md=20?= =?UTF-8?q?=E2=80=94=20T1=20docker=20run=20+=20T2=20compose,=20zero=20to?= =?UTF-8?q?=20first=20call=20(slice-G)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The acceptance-bar doc: a copy-paste path from a bare Linux host with a domain to (1) a browser WebRTC call and (2) a real PSTN call via the Twilio webhook. Volumes marked non-negotiable with the LE-lockout reason; all-or-none RUTSTER_TWILIO_* contract stated (matches config.rs); host networking recommended for direct media UDP. Signed-off-by: Aaron D. Lee --- docs/deploy/quickstart-docker.md | 104 +++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 docs/deploy/quickstart-docker.md diff --git a/docs/deploy/quickstart-docker.md b/docs/deploy/quickstart-docker.md new file mode 100644 index 0000000..61da1d8 --- /dev/null +++ b/docs/deploy/quickstart-docker.md @@ -0,0 +1,104 @@ +# Docker quickstart — zero to first call + +Two shapes, one binary ([topologies.md](topologies.md)). T1 is one `docker run`; T2 is the +reference `compose up`. Both end the same way: a browser call at your domain, and (with Twilio +credentials) a real phone call. + +> **Artifact names note:** images are published to the `git.adlee.work/alee/` registry +> namespace by the release CI (plan B / slice F settled this). If your Gitea registry uses a +> different owner segment, substitute accordingly — check `deploy/compose.yaml` in your +> checkout, which is authoritative. + +## Prerequisites (both shapes) + +1. A Linux host with Docker, a **public IPv4**, and inbound `80/tcp`, `443/tcp`, and your + chosen media UDP range open. (No public IP? → [homelab.md](homelab.md).) +2. A domain with an A record pointing at that host, e.g. `pbx.example.com`. Certificates are + issued automatically via ACME — no self-signed path exists, the CPaaS refuses it + ([certificates.md](certificates.md)). +3. For the phone call: a [Twilio](https://www.twilio.com/) account + a Voice-capable number + (trial works). + +## T1 — Solo (all-in-one) + +```bash +docker run -d --name rutster --restart unless-stopped \ + --network host \ + -v rutster-caddy-data:/data \ + -v rutster-valkey:/var/lib/valkey \ + -e RUTSTER_DOMAIN=pbx.example.com \ + -e RUTSTER_ACME_EMAIL=you@example.com \ + -e RUTSTER_MEDIA_ADVERTISED_IP=203.0.113.7 \ + -e RUTSTER_MEDIA_PORT_RANGE=49152-49407 \ + -e RUTSTER_TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ + -e RUTSTER_TWILIO_AUTH_TOKEN=your_auth_token \ + -e RUTSTER_TWILIO_MEDIA_BIND=0.0.0.0:8081 \ + -e RUTSTER_TWILIO_WEBHOOK_BASE=https://pbx.example.com \ + git.adlee.work/alee/rutster-allinone:latest +``` + +Notes: + +- `--network host` is the recommended mode: WebRTC media UDP goes **direct to the engine**, + never through Caddy. If you can't use host networking, publish `-p 80:80 -p 443:443 + -p 49152-49407:49152-49407/udp` instead and keep `RUTSTER_MEDIA_ADVERTISED_IP` = the host's + public IP (NAT 1:1, no STUN). +- The two volumes are **non-negotiable**. `/data` in particular: recreating the container + without it re-requests certificates and can hit Let's Encrypt's duplicate-certificate + limit — a week-long total inbound outage ([certificates.md](certificates.md)). +- The four `RUTSTER_TWILIO_*` vars are all-or-none — the engine refuses partial Twilio config + at startup. Omit all four to run WebRTC-only. +- `RUTSTER_DOMAIN` / `RUTSTER_ACME_EMAIL` feed the bundled Caddyfile. Domains that can't do + HTTP-01 (no port 80, wildcard needs) use DNS-01 — [certificates.md](certificates.md). + +Verify it's up: + +```bash +curl -fsS https://pbx.example.com/healthz && echo OK # liveness +curl -fsS https://pbx.example.com/readyz && echo READY # can accept a new call +``` + +### First call (browser, WebRTC) + +Open `https://pbx.example.com/`, click **Start call**, grant the microphone. You're on a call +with the engine. + +### First call (phone, PSTN) + +1. Twilio Console → Phone Numbers → your number → **A call comes in** → Webhook, + `POST https://pbx.example.com/v1/trunk/webhook`. +2. Dial your Twilio number from any phone. Twilio hits the webhook, the engine answers with + TwiML pointing Twilio's Media Streams at `wss://pbx.example.com/twilio/media-stream`, and + the call's audio enters the same reflex loop a WebRTC call uses. + +## T2 — Modular (compose stack) + +```bash +git clone https://git.adlee.work/alee/rutster.git +cd rutster/deploy +cp .env.example .env +$EDITOR .env # set DOMAIN, ACME email, RUTSTER_MEDIA_ADVERTISED_IP, RUTSTER_TWILIO_* +docker compose up -d +``` + +What comes up: `caddy` (edge, `rutster-edge`), `engine` (`rutster-engine`), `brain` +(`rutster-brain`, sharing the engine's network namespace), `valkey` (upstream image). The +same two first-call paths as T1 apply verbatim. + +Operational notes: + +- `.env.example` ships `RUTSTER_DRAIN_DEADLINE_SECS=600` and the stack sets + `stop_grace_period: 660s` — grace exceeds drain, so `docker compose down` lets in-flight + calls finish (up to 10 minutes) instead of killing them. +- Upgrade: `docker compose pull && docker compose up -d`. +- Bring your own proxy instead of Caddy: [reverse-proxies.md](reverse-proxies.md). + +## Troubleshooting + +| Symptom | Cause / fix | +|---|---| +| Twilio error **31910** on calls | Your cert isn't publicly trusted or expired — check `/data` volume survived, see [certificates.md](certificates.md) ([twilio.com/docs/api/errors/31910](https://www.twilio.com/docs/api/errors/31910)) | +| Webhook signature validation fails | The edge isn't forwarding `X-Forwarded-Proto/Host` honestly, or `RUTSTER_TRUSTED_PROXIES` doesn't include your proxy — the engine ignores forwarded headers from unlisted sources | +| Browser call connects, no audio | Media UDP blocked or `RUTSTER_MEDIA_ADVERTISED_IP` wrong — the SDP advertises that IP; callers send UDP straight to it | +| `curl /readyz` returns 503 | Node is draining or at the admission cap (`RUTSTER_MAX_SESSIONS`) — liveness (`/healthz`) stays 200 | +| Let's Encrypt rate-limit errors in Caddy logs | You recreated the container without the `/data` volume — [certificates.md](certificates.md) |