docs(deploy): quickstart-docker.md — T1 docker run + T2 compose, zero to first call (slice-G)

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 <himself@adlee.work>
This commit is contained in:
2026-07-05 22:33:23 -04:00
parent 285f2055ba
commit 1be6112148

View File

@@ -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) |