Setup
Behind a reverse proxy
Run Tyto behind an existing reverse proxy — plain HTTP mode, ports, WebSockets, SSE and voice.
Both stacks bundle Caddy with automatic HTTPS and want ports 80/443. When those ports already belong to a reverse proxy on the same host (nginx, Traefik, Caddy, a NAS ingress), switch Tyto to plain-HTTP mode and let your proxy terminate TLS.
Core
Add to the core .env:
BEHIND_PROXY=true
HTTP_PORT=127.0.0.1:8080
SERVER_DOMAIN stays your real public domain — all URLs still derive from it
and requests are host-matched. BEHIND_PROXY=true disables certificate
issuance and serves plain HTTP; HTTP_PORT picks the host port your proxy
forwards to. Bind it to 127.0.0.1 when the proxy runs on the same host so the
plain-HTTP backend isn’t exposed on every interface — use 0.0.0.0:8080 only
when the proxy is on another machine. TRUSTED_PROXIES defaults to
private_ranges so client IPs and scheme resolve from the proxy’s forwarded
headers — set your proxy’s exact CIDR to narrow it.
Client
Same two lines in the client .env:
BEHIND_PROXY=true
HTTP_PORT=127.0.0.1:8081
Running the single-stack bundle instead? Just the core config
applies — both domains ride the same HTTP_PORT, host-matched.
What the proxy must do
- Forward the original
Hostheader (requests are matched by domain). - Support WebSocket upgrade on the API domain — real-time and the voice
signal ride the API origin (
/rtc). - Be SSE-friendly on the API domain: response buffering off and a long
read timeout (Mercure keeps connections open). For nginx:
proxy_buffering off;and a highproxy_read_timeout.
A host-level Caddy needs none of the extra tuning — its defaults handle WebSockets and SSE.
chat.example.com {
reverse_proxy localhost:8080
}
app.example.com {
reverse_proxy localhost:8081
}
For nginx, a minimal API-domain location block:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 24h;
}
($connection_upgrade comes from the standard
map $http_upgrade $connection_upgrade block in nginx.conf.)
Voice media bypasses the proxy
Only the voice signal (WebSocket) goes through the proxy. The media itself
is WebRTC: the server advertises its public IP via ICE and browsers connect
directly — 7881/tcp + 50000–50100/udp must reach the core host, no proxy
involved (no L7 proxy can carry WebRTC media; this is true of every voice
platform). If those ports can’t be opened, set VOICE_ENABLED=false for a
text-only server.
If your goal is hiding the origin IP (e.g. behind a CDN), voice leaks it — ICE candidates contain the host’s real public IP. Text-only mode keeps the origin hidden.
Media is still encrypted end-to-end at the transport level (DTLS-SRTP); it does not need TLS certificates or a domain.