Skip to main content

Ingress: Cloudflare Tunnel

The rule everything else is built around

A Cloudflare Tunnel carries HTTP and WebSocket. It does not carry UDP, and WebRTC media is UDP. Putting a media server behind the tunnel means smuggling media around it somehow — every one of those workarounds breaks. Peer-to-peer media never has this problem, because media never goes near the tunnel at all. See Peer-to-Peer Media.

Signalling: Client → Cloudflare → Tunnel → Nginx → call-service
(WebSocket, same shape as chat)

Media: Client <========================================> Client
(WebRTC, direct, never touches Cloudflare)

What this buys

  • No UDP port is ever opened.
  • No service ever advertises its own address. There is deliberately no "advertised address" setting anywhere — that class of bug (correct on the server, wrong for every client, invisible to whatever the operator can test locally) was the single most expensive bug in an earlier design that used an SFU.
  • One hostname. Signalling is a WebSocket path on the same gateway as everything else.
  • Reachable from any network a client can reach the site from at all.

Reaching the other peer

CaseWhat carries the media
Same LANDirect, host candidates
Different networks, ordinary NATDirect, STUN-discovered addresses
Symmetric / carrier-grade NATTURN relay, only if one is configured

STUN needs no tunnel and no port — the client dials a public STUN server itself. TURN is optional and off by default; when an operator wants that last category of network to work, Cloudflare's own TURN service fits naturally since it's outbound-only too, and call-service mints short-lived credentials for it per call.

Two layers, two jobs

Cloudflare Tunnel:

  • Secure outbound tunnel
  • Public ingress for HTTP and WebSocket
  • No port forwarding, no direct public server exposure

Nginx / Traefik:

  • Internal routing, load balancing
  • WebSocket routing, rate limiting
  • Service routing

cloudflared can run as its own Docker container (--profile public), or an operator can point an existing tunnel they already run at http://localhost:8080 — the gateway's published GATEWAY_PORT. Both work; the difference is one line of tunnel config.

Route map

/ web client (apps/web, built static bundle)
/admin admin panel (apps/admin)
/api/v1/* REST, routed to the owning service
/ws/* WebSocket, routed to the owning service's gateway

Nginx matches the longest prefix, so / never shadows /admin, /api, or /ws.