Files
rutster/docs/deploy/quickstart-docker.md
Aaron D. Lee 1be6112148 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>
2026-07-05 22:33:23 -04:00

5.1 KiB

Docker quickstart — zero to first call

Two shapes, one binary (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.)
  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).
  3. For the phone call: a Twilio account + a Voice-capable number (trial works).

T1 — Solo (all-in-one)

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).
  • 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.

Verify it's up:

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)

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.

Troubleshooting

Symptom Cause / fix
Twilio error 31910 on calls Your cert isn't publicly trusted or expired — check /data volume survived, see certificates.md (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