Files
rutster/docs/deploy/reverse-proxies.md
2026-07-06 04:07:15 +00:00

5.3 KiB
Raw Permalink Blame History

Bring your own reverse proxy — tuned-timeout configs

Supported: disable the caddy service in the compose stack (quickstart-docker.md); 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 ~60180 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).
  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).
  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).

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). rutster ships this snippet and nothing more for 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). 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.

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, the v2.11.2 timeout-behavior flip, and #11405. Pin the exact image tag and never auto-update the edge.

# 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
# 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: browser at https://pbx.example.com/, Twilio webhook at https://pbx.example.com/v1/trunk/webhook.