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 Host header (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 high proxy_read_timeout.
Tip

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.

Warning

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.