deploy-epoch: deployment topology — one binary, three blessed shapes (#27)
Some checks failed
CI / fmt (push) Successful in 1m10s
CI / clippy (push) Successful in 8m17s
CI / test (1.85) (push) Failing after 9m49s
CI / test (stable) (push) Failing after 17m18s
CI / deny (push) Failing after 1m51s
CI / sim-bench (stable) (push) Successful in 24m52s
CI / image-build (4 images) (push) Has been skipped
CI / smoke (all-in-one TLS sim call) (push) Has been skipped
CI / smoke (T2 compose four-service) (push) Has been skipped
CI / smoke (caddy reload during live call, zero drops) (push) Has been skipped
CI / twilio-live (manual only) (push) Has been skipped

deploy-epoch: deployment topology — one binary, three blessed shapes

The full deploy-epoch integration: deployment topology spec + 4 implementation slices
(69 files, +14771/-36). Adds Docker image build pipeline (4 named --target stages),
container smoke CI (all-in-one TLS + compose + caddy reload-during-call), rustls
Phase 1 in-process TLS, /metrics endpoint, ValkeyEventSink, engine hygiene fixes
(TCP_NODELAY + TCP_QUICKACK-suppression + WS pings + trunk config), and the slice-F
tag-push image publish workflow. ADR-0011 (deployment topology) Proposed.

Pre-tag verification (PM-executed on integration tip c772485):
- Seam gate intact (loop_driver + rtc_session hashes unchanged)
- cargo fmt/clippy/test/doc all exit 0
- cargo deny bans+licenses FAILED pre-existing at main 6340e63 (CI authoritative
  via cargo-deny-action@v2 with cargo-deny 0.19.x)

PRs merged into this commit: #23 (dev-d/G), #24 (dev-a/A), #25 (dev-c/C+D+E), #26 (dev-b/B+F)
Integration PR: #27

Signed-off-by: Aaron D. Lee <himself@adlee.work>
Co-authored-by: Aaron D. Lee <himself@adlee.work>
Co-committed-by: Aaron D. Lee <himself@adlee.work>
This commit was merged in pull request #27.
This commit is contained in:
2026-07-09 21:11:25 +00:00
committed by A.D.Lee
parent 6340e638d5
commit cc25e80310
69 changed files with 14771 additions and 36 deletions

148
deploy/.env.example Normal file
View File

@@ -0,0 +1,148 @@
# deploy/.env.example — runtime configuration for the rutster T1 (all-in-one)
# + T2 (compose) artifacts. Copy to `.env` + edit before `docker compose up -d`.
#
# Format: KEY=value (one per line); comments (#) are ignored by `docker
# compose --env-file`. Fail-fast pattern: every var is parsed by
# crates/rutster/src/config.rs's pure-`Option<String>` parsers at startup;
# an invalid value fails the boot loudly with the var name in the error,
# never silently runs with a wrong default. See AGENTS.md "config.rs"
# pattern.
#
# Bundled-binary licenses (aggregation-clean vs GPL-3 per ADR-0004):
# * Caddy Apache-2.0
# * valkey BSD-3
# * s6-overlay ISC
# * libopus0 BSD-3 (system package; Debian)
# Future cargo-deny-adjacent image license scan will assert these in CI
# (spec §6.1 — out of scope for slice B; this comment seeds it).
#
# Registry namespace: git.adlee.work/alee/rutster-* (matches the `alee` tea
# login per AGENTS.md Git workflow + the root Cargo.toml's `repository`
# field). If your Gitea registry uses a different owner segment, replace
# `alee` throughout deploy/compose.yaml + .github/workflows/publish-images.yml.
################################################################################
# ARTIFACT-LEVEL — Caddy edge (this slice)
################################################################################
# RUTSTER_DOMAIN — the public hostname the CPaaS dials. Required for T1 (solo
# all-in-one) and T2 (compose). Used by Caddyfile as the site block name.
# Example: pbx.example.com
RUTSTER_DOMAIN=pbx.example.com
# RUTSTER_ACME_EMAIL — the Let's Encrypt account email. Used by Caddy for
# ACME account creation. Required for production (ACME issuance); ignored
# in CI (RUTSTER_LOCAL_CERTS=true bypasses ACME).
RUTSTER_ACME_EMAIL=you@example.com
# RUTSTER_LOCAL_CERTS — set to "true" in CI to use Caddy's internal CA (no
# ACME round-trips, no rate-limit budget burns). Production leaves this
# unset (or "false") so Caddy uses Let's Encrypt. The Caddyfile reads this
# via the {$RUTSTER_LOCAL_CERTS:local_certs_off} interpolation.
# RUTSTER_LOCAL_CERTS=false
################################################################################
# FOB engine — bind + drain (slice-5 config.rs + plan-A)
################################################################################
# RUTSTER_HTTP_BIND — the FOB's HTTP/WS listener. Behind Caddy (T1/T2 prod):
# 0.0.0.0:8080 (compose network) or 127.0.0.1:8080 (single container T1).
# Default if unset: 0.0.0.0:8080 (config.rs http_bind).
RUTSTER_HTTP_BIND=0.0.0.0:8080
# RUTSTER_DRAIN_DEADLINE_SECS — how long shutdown waits for in-flight calls
# before the hard stop. Default 0 = instant shutdown (dev loop); production
# sets minutes. Spec §2.2 pins 600 (paired with compose stop_grace_period
# 660s — grace MUST exceed drain or the orchestrator kills calls the
# engine was gracefully draining).
RUTSTER_DRAIN_DEADLINE_SECS=600
# RUTSTER_MAX_SESSIONS — admission cap. Default 64 (placeholder until the
# ADR-0010 benchmark measures the real per-node ceiling). /readyz flips 503
# when at the cap so the LB pulls the node.
RUTSTER_MAX_SESSIONS=64
################################################################################
# FOB engine — WebRTC media (slice-5 config.rs media_address_config)
################################################################################
# RUTSTER_MEDIA_BIND_IP — the IP WebRTC DTLS-SRTP binds. Default 127.0.0.1
# (loopback — dev loop). Production hosts a public IP.
RUTSTER_MEDIA_BIND_IP=0.0.0.0
# RUTSTER_MEDIA_ADVERTISED_IP — the IP advertised in SDP to WebRTC callers.
# They send media UDP straight to this IP (NAT 1:1 — no STUN, no TURN).
# In T1 (host networking): the host's public IP. In T2 (compose): the
# engine container's published port's host IP.
RUTSTER_MEDIA_ADVERTISED_IP=203.0.113.7
# RUTSTER_MEDIA_PORT_RANGE — UDP port range for WebRTC media. Format lo-hi
# inclusive. Must be open inbound on the host's firewall + published in
# compose (deploy/compose.yaml publishes this range).
RUTSTER_MEDIA_PORT_RANGE=49152-49407
################################################################################
# FOB engine — WS hygiene (plan-A §5.2)
################################################################################
# RUTSTER_WS_PING_SECS — engine-originated app-level WS ping interval on
# the trunk media-stream WS. Default 20s. 0 rejected (would busy-loop the
# ping timer); no "off" spelling by design.
RUTSTER_WS_PING_SECS=20
################################################################################
# FOB engine — trusted-proxy posture (plan-A §5.3)
################################################################################
# RUTSTER_TRUSTED_PROXIES — comma-separated CIDR list of edge peers whose
# X-Forwarded-Proto/Host are believed. Empty or unset = headers IGNORED
# (fail-closed default). Bare IPs accepted as host-length prefixes. In T1/T2
# with Caddy as edge, set to the Caddy container's source IP (compose:
# the compose network's subnet CIDR; T1: 127.0.0.1/32). An internet peer
# must never choose the public URL that Twilio signature validation
# reconstructs (spec §3.1 invariant 5).
RUTSTER_TRUSTED_PROXIES=127.0.0.1/32
################################################################################
# Trunk — Twilio credentials (slice-5 config.rs twilio_credentials)
################################################################################
# All four RUTSTER_TWILIO_* vars are ALL-OR-NONE: the parser rejects partial
# config at startup. Omit all four to run WebRTC-only.
RUTSTER_TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
RUTSTER_TWILIO_AUTH_TOKEN=your_auth_token_here
RUTSTER_TWILIO_MEDIA_BIND=0.0.0.0:8081
RUTSTER_TWILIO_WEBHOOK_BASE=https://pbx.example.com
################################################################################
# Brain — tap URL (slice-2/3; loopback-only until step 6)
################################################################################
# RUTSTER_TAP_URL — the WS URL the FOB opens to the brain. Default
# ws://127.0.0.1:8081/echo (slice-2 dev-loop echo brain). Production with
# rutster-brain-realtime: ws://127.0.0.1:8082 (loopback-only —
# resolve_tap_url rejects non-loopback until step 6's wss:// tap). In T2
# (compose), the brain shares the engine's netns
# (network_mode: "service:engine") so this loopback IS the engine's
# loopback.
RUTSTER_TAP_URL=ws://127.0.0.1:8082
################################################################################
# Future vars — land with sibling slices, NOT this slice
################################################################################
# RUTSTER_TLS_CERT / RUTSTER_TLS_KEY — land with slice C (rustls Phase 1,
# BYO-cert in-process TLS). The engine binary stays compiled-with-rustls
# from slice C onward; runtime-gated by these vars. See the slice-C plan
# at docs/superpowers/plans/2026-07-05-deploy-c-rustls-tls.md (path may
# vary — check the plans dir).
# RUTSTER_TLS_CERT=/etc/rutster/tls/fullchain.pem
# RUTSTER_TLS_KEY=/etc/rutster/tls/privkey.pem
# RUTSTER_METRICS_BIND — lands with slice D (/metrics endpoint). Default
# 127.0.0.1:9090 (internal — never routed through Caddy).
# RUTSTER_METRICS_BIND=127.0.0.1:9090
# RUTSTER_VALKEY_URL — lands with slice E (ValkeyEventSink). Unset = today's
# TracingEventSink (logs to stdout). Set to redis://valkey:6379/0 in T2.
# RUTSTER_VALKEY_URL=redis://valkey:6379/0