Cloudflare Tunnel
Host Gryt behind Cloudflare Tunnel (HTTP) + direct UDP (WebRTC media)
This setup uses Cloudflare Tunnel for HTTPS/WSS (server, SFU websocket) while keeping WebRTC media UDP direct-to-host.
What Cloudflare can and can’t proxy
- OK via Tunnel:
- Server HTTP + WebSocket (Socket.IO)
- SFU websocket (HTTP upgraded to WSS at the edge)
- Not OK via Tunnel:
- WebRTC media (UDP). You must open/forward a UDP port range to the SFU host.
Important: Tunnel/proxy is only for the SFU WebSocket
Cloudflare Tunnel (and Cloudflare “orange cloud” proxying) only affects the signaling/control connection to the SFU over HTTP/WSS. Your actual voice traffic (ICE + DTLS + SRTP) still flows directly over UDP to the SFU host.
That means:
SFU_PUBLIC_HOSTcan point to a tunneled/proxied hostname (e.g.wss://sfu.example.com) because it’s only used for the WebSocket.- Your ICE candidates must advertise real, reachable IPs for the SFU host (set
ICE_ADVERTISE_IP). - You must open the corresponding UDP ports on the SFU host firewall / security group (see below).
If dig AAAA sfu.example.com returns Cloudflare anycast IPs (often starting with 2606:4700:...), that’s expected for a proxied hostname — but it is not where WebRTC UDP will go.
Recommended compose stack
Use the self-contained hosting stack:
ops/deploy/host/compose.ymlops/deploy/host/.env(create from.env.example)
Configure
cd ops/deploy/host
cp .env.example .envEdit ops/deploy/host/.env (minimum):
JWT_SECRET="replace-me" # openssl rand -base64 48
SFU_PUBLIC_HOST="wss://sfu.example.com"
CORS_ORIGIN="http://127.0.0.1:15738,https://app.gryt.chat"Start
docker compose -f ops/deploy/host/compose.yml up -d --buildCloudflared routing (example)
Map hostnames to localhost origins:
api.gryt.example.com→http://127.0.0.1:5000(WebSocket upgrade required)sfu.example.com→http://127.0.0.1:5005(WebSocket upgrade required)
Cloudflare Tunnel terminates TLS at the edge, so your public endpoints are https://... / wss://... even if the services on the host are plain HTTP.
Example cloudflared config.yml:
tunnel: <your-tunnel-id>
credentials-file: /etc/cloudflared/<your-tunnel-id>.json
ingress:
- hostname: api.gryt.example.com
service: http://127.0.0.1:5000
originRequest:
noTLSVerify: true
connectTimeout: 10s
keepAliveTimeout: 90s
- hostname: sfu.example.com
service: http://127.0.0.1:5005
originRequest:
noTLSVerify: true
connectTimeout: 10s
keepAliveTimeout: 90s
- service: http_status:404WebSocket connection stability
Cloudflare Tunnel may occasionally reset long-lived WebSocket connections during edge server rotation or tunnel reconnection. Gryt handles this automatically: the signaling server preserves voice state for up to 15 seconds during transient disconnects, and the client will recover without tearing down the SFU media connection if it is still alive. Users may briefly see a "Reconnecting" toast but voice/screenshare should continue uninterrupted.
The originRequest settings above help reduce spurious disconnects:
noTLSVerify-- skip TLS verification for the local connection (the tunnel already terminates TLS at the edge).connectTimeout-- time to establish a TCP connection to the origin (default 30s, 10s is faster for local services).keepAliveTimeout-- idle timeout before discarding keepalive connections (default 90s). Gryt's Socket.IO sends pings every 15s, so connections should never go idle.
DNS records: enable Proxied (orange cloud) for all hostnames
Every hostname routed through the tunnel must have its DNS record set to Proxied (orange cloud) in the Cloudflare dashboard. This includes the SFU hostname.
| Hostname | Type | Content | Proxy status |
|---|---|---|---|
api.gryt.example.com | CNAME | <tunnel-id>.cfargotunnel.com | Proxied (orange) |
sfu.example.com | CNAME | <tunnel-id>.cfargotunnel.com | Proxied (orange) |
Don't grey-cloud the SFU hostname. Even though WebRTC media is direct UDP,
the SFU WebSocket (port 5005) still goes through the tunnel. If you set sfu.example.com to
DNS-only (grey cloud), browsers won't be able to reach the SFU WebSocket and voice connections will fail.
Upload size limit (100 MB)
Cloudflare enforces a 100 MB maximum request body size on Free and Pro plans (200 MB on Business, 500 MB on Enterprise). File uploads that exceed this limit are rejected by Cloudflare's edge before they reach your server. Because Cloudflare's error response does not include your server's CORS headers, the browser reports a misleading CORS error instead of the real cause.
This affects all HTTP uploads routed through the tunnel — chat file attachments, avatars, and emoji uploads.
Large file uploads fail silently
If a user uploads a file larger than 100 MB through a Cloudflare Tunnel on a Free/Pro plan, they will see a generic CORS or network error. The request never reaches your Gryt server, so there will be no log entry on the server side.
Alternatives for large uploads
If you need to support file uploads larger than 100 MB, consider using a different reverse proxy instead of (or alongside) Cloudflare Tunnel:
- Nginx Proxy Manager — web-based UI for managing Nginx reverse proxies with automatic Let's Encrypt certificates. Set
client_max_body_sizeto your desired limit. - Traefik — cloud-native reverse proxy with automatic TLS. No default body size limit.
- Caddy — simple reverse proxy with automatic HTTPS (see the Docker Compose guide). No default body size limit.
All three options handle TLS termination and have no inherent upload size restrictions, so the only limit will be what you configure on the Gryt server itself (default: 200 MB).
Firewall / port forwarding (critical)
Open/forward UDP to the host running the sfu container.
Recommended (best success rate on restrictive networks):
ICE_UDP_MUX_PORT/udp(typically443/udp)
Alternative:
SFU_UDP_MIN..SFU_UDP_MAX/udp(default10000-10019/udp)
If the SFU host is behind NAT or has multiple interfaces, set ICE_ADVERTISE_IP so it advertises the correct public IP in ICE candidates. For multi-network setups (e.g. LAN + public), both ICE_ADVERTISE_IP and SFU_PUBLIC_HOST accept comma-separated values — the client automatically pings each endpoint and picks the fastest one.
For IPv6-heavy client networks (common on mobile), make sure the SFU host has IPv6 and include the public IPv6 in ICE_ADVERTISE_IP, and allow inbound IPv6 UDP on the same media port(s).