# Bring your own reverse proxy — tuned-timeout configs Supported: disable the `caddy` service in the compose stack ([quickstart-docker.md](quickstart-docker.md#t2--modular-compose-stack)); the engine keeps its plaintext `:8080` listener. Everything below terminates TLS in *your* proxy and forwards to the engine. ## The universal 60-second footgun **nginx, HAProxy, and Traefik all default their idle/read timeouts to ~60–180 seconds — every one of them will kill a quiet, perfectly healthy call WebSocket.** A Media Streams WS can legitimately be near-silent in one direction for hours, and neither Twilio nor Telnyx sends protocol-level keepalives. The engine ships app-level WS pings (`RUTSTER_WS_PING_SECS`, default 20) as belt-and-braces, but your proxy timeouts must still exceed the maximum call duration. Every snippet below does that. ## The contract any proxy must honor 1. **Publicly-trusted cert**, auto-renewed — no self-signed path exists ([certificates.md](certificates.md)). 2. **WS upgrade with no frame buffering and no connection-lifetime cap** — 20 ms audio frames, calls up to 24 h. 3. **Honest `X-Forwarded-Proto` and `X-Forwarded-Host`** (including on the WS upgrade request): `X-Twilio-Signature` is HMAC over the URL as Twilio saw it ([Twilio's SSL-termination guidance](https://www.twilio.com/en-us/blog/developers/tutorials/building-blocks/handle-ssl-termination-twilio-node-js-helper-library)). 4. **Engine-side trust**: set `RUTSTER_TRUSTED_PROXIES` to your proxy's source IP/CIDR — forwarded headers from unlisted sources are ignored, and signature validation will fail. 5. **TLS 1.2+1.3, mainstream ECDHE — never 1.3-only** (Twilio's 1.3 client support is undocumented). ## nginx (works if you insist — not a recommended edge) Why not recommended: no native DNS-01/wildcard support, and cert renewal requires a reload whose old workers linger against hours-long WS connections (research basis: [TLS brief §4](../superpowers/specs/2026-07-05-tls-edge-decision-brief.md)). rutster ships this snippet and nothing more for nginx. ```nginx map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 443 ssl; server_name pbx.example.com; ssl_certificate /etc/letsencrypt/live/pbx.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/pbx.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; # never 1.3-only location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_buffering off; # 20 ms frames must not be coalesced proxy_read_timeout 86400s; # the 60s default is the footgun proxy_send_timeout 86400s; } } ``` ## HAProxy (3.2 LTS or newer) The best WS mental model of the classic family: after the upgrade, only `timeout tunnel` governs the connection — **but only if you set it**; put it in `defaults` or it silently doesn't apply to the paths you think it does (see [haproxy#2280](https://github.com/haproxy/haproxy/issues/2280)). Two extra notes: `acme.sh` can hot-load renewed certs through the stats socket with zero reload; and `hard-stop-after` kills calls that are still draining — leave it unset or above your drain deadline. ```haproxy defaults mode http timeout connect 5s timeout client 75s timeout server 75s timeout tunnel 24h # governs upgraded WS; unset = the 60s-class footgun frontend fe_rutster bind :443 ssl crt /etc/haproxy/certs/ ssl-min-ver TLSv1.2 http-request set-header X-Forwarded-Proto https http-request set-header X-Forwarded-Host %[req.hdr(Host)] default_backend be_rutster backend be_rutster server fob 127.0.0.1:8080 ``` ## Traefik v3 — PIN THE EXACT VERSION Traefik has the best built-in DNS-01/wildcard of the classic family **and a track record of breaking WebSockets in patch releases**: [#10601](https://github.com/traefik/traefik/issues/10601), the v2.11.2 timeout-behavior flip, and [#11405](https://github.com/traefik/traefik/issues/11405). **Pin the exact image tag and never auto-update the edge.** ```yaml # traefik.yml (static config) entryPoints: websecure: address: ":443" transport: respondingTimeouts: readTimeout: 0 # 0 disables; the 60s-class default kills quiet WS idleTimeout: 0 writeTimeout: 0 certificatesResolvers: le: acme: email: you@example.com storage: /data/acme.json # persist this volume — certificates.md dnsChallenge: provider: cloudflare ``` ```yaml # dynamic config http: routers: rutster: rule: "Host(`pbx.example.com`)" entryPoints: [websecure] service: rutster tls: { certResolver: le } services: rutster: loadBalancer: servers: - url: "http://127.0.0.1:8080" ``` Traefik forwards `X-Forwarded-Proto/Host` by default — you still must list its address in `RUTSTER_TRUSTED_PROXIES`. ## First call Any of the above in front of the engine, then the same two first-call paths as [quickstart-docker.md](quickstart-docker.md): browser at `https://pbx.example.com/`, Twilio webhook at `https://pbx.example.com/v1/trunk/webhook`.