Files
rutster/docs/deploy/topologies.md
Aaron D. Lee 285f2055ba docs(deploy): topologies.md — three blessed shapes + decision guide (slice-G)
The docs/deploy/ tree entry point. T1/T2 process+service tables, the
grace-exceeds-drain rule, the honest T3 statement (paper only, no
copy-paste deploy exists), the not-supported list, and the shared
ports/volumes reference. Acceptance bar: copy-paste paths delegate to
quickstart-docker.md; T3 states plainly what cannot be done today.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 22:22:34 -04:00

5.8 KiB

Deployment topologies — one binary, three blessed shapes

The FOB never decomposes; topology is a configuration property, not a code property. The same binary runs in every shape below; only RUTSTER_* env and what is deployed alongside it differ. Ratified (Proposed) in ADR-0011.

Decision guide

You are… Use Doc
Trying rutster for the first time; one box, one domain, a public IP T1 — Solo quickstart-docker.md
Running production and want independently upgradable parts T2 — Modular (the reference deployment) quickstart-docker.md
Behind CGNAT / no public IP (homelab) T1/T2 + tunnel (dev, PSTN-only) or VPS forwarder (production) homelab.md
Already running nginx / HAProxy / Traefik T2 minus the caddy service reverse-proxies.md
On EC2 and want ALB + ACM to terminate TLS T2/T3 behind an ALB aws.md
Outgrowing one node T3 — Fleetpaper only today ADR-0011

T1 — Solo (all-in-one container)

One image, rutster-allinone, with s6-overlay supervising four processes:

Process Role Binding
caddy Edge: ACME issuance/renewal, TLS termination, WS proxy :443 / :80 public
rutster (the FOB) Engine — signaling, trunk webhook, media, reflexes 127.0.0.1:8080 behind Caddy; media UDP direct
rutster-brain-realtime Brain 127.0.0.1:8082 loopback tap
valkey-server Event stream (EventSink); ledger/directory later 127.0.0.1:6379

Two volumes, both non-negotiable:

  • /data — Caddy's cert/ACME state. Losing it can lock your domain out of Let's Encrypt for up to a week (duplicate-certificate limit) — a total inbound outage. See certificates.md.
  • /var/lib/valkey — stream/state persistence.

WebRTC media (UDP) goes direct to the FOB, never through Caddy: run with host networking, or publish RUTSTER_MEDIA_PORT_RANGE and set RUTSTER_MEDIA_ADVERTISED_IP to your public IP (NAT 1:1 model — no STUN).

Copy-paste path from zero to first call: quickstart-docker.md.

T2 — Modular (compose stack) — the reference deployment

Four services in deploy/compose.yaml:

Service Image Notes
caddy rutster-edge Same custom Caddy build + same Caddyfile as T1
engine rutster-engine The FOB; plaintext :8080 on the compose network; media UDP published
brain rutster-brain network_mode: "service:engine" — shares the engine's netns so the loopback-only tap posture survives unchanged
valkey valkey/valkey (upstream) Own service; network-near

Operational contract:

  • stop_grace_period: 660s paired with RUTSTER_DRAIN_DEADLINE_SECS=600 — grace must exceed drain, or the orchestrator kills calls the engine was gracefully draining.
  • Bring-your-own-proxy: disable the caddy service; the engine keeps its plaintext :8080 mode. Tuned-timeout configs: reverse-proxies.md.
  • Upgrades: docker compose pull && docker compose up -d — the drain window lets in-flight calls finish (calls are non-migratable; a killed engine kills its calls, honestly stated).

Copy-paste path from zero to first call: quickstart-docker.md.

T3 — Fleet (paper only — no shipped artifacts in this epoch)

Honest statement: you cannot deploy T3 today by copy-paste; there are no fleet artifacts yet. The design is ratified on paper in ADR-0011 so single-node deployments grow toward it, not away from it:

  • N symmetric nodes (the T1 process set minus valkey) + a shared green zone: one Valkey (presence + directory + spend ledger), object storage.
  • Wildcard DNS *.pbx.domain; per-node certs — the traps live in certificates.md.
  • Answer-time placement via Valkey presence; node-addressed wss://node-K.pbx.domain/... Stream URLs; no router tier.
  • Per-node admission cap = the benchmark number (RUTSTER_MAX_SESSIONS, default 64, is a placeholder until then).
  • Scale-in is drain-then-terminate only. No spot instances for the engine tier.

What is deliberately not supported

  • Splitting the FOB into services — permanent rejection, not a roadmap item.
  • Serverless — non-migratable multi-hour calls, per-session UDP sockets, in-process media state. Categorically unfit.
  • Tunnels in production — plaintext audio at the tunnel vendor's edge + documented mid-call WS kills. Dev/demo only; the whole story is in homelab.md.
  • NLB TLS listeners — see aws.md for the 350-second reason.
  • k8s manifests/Helm — compose-first. If you run k8s anyway: set terminationGracePeriodSeconds above RUTSTER_DRAIN_DEADLINE_SECS, same grace-exceeds-drain rule as compose.

Ports & volumes reference (all shapes)

Surface Where Notes
:80 / :443 TCP Caddy, public ACME HTTP-01 + TLS + all HTTPS/WSS traffic
:8080 TCP FOB, behind the edge Signaling + /v1/trunk/webhook + /twilio/media-stream WS — one listener (RUTSTER_HTTP_BIND)
RUTSTER_MEDIA_PORT_RANGE UDP FOB, public, direct WebRTC media; never proxied
:8082 TCP Brain, loopback only The tap; non-loopback rejected until step 6
:6379 TCP Valkey, private Loopback (T1) / compose network (T2) / green zone (T3)
:9090 TCP FOB /metrics, internal RUTSTER_METRICS_BIND; never routed through Caddy
/data volume Caddy Cert/ACME state — loss class: total inbound outage
/var/lib/valkey volume Valkey Event stream persistence