# 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_TRUSTED_PROXIES=127.0.0.1/32 \
  -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. Note: signature enforcement is **planned, NOT yet enforced** — the webhook surface is currently unauthenticated |
| 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) |
