Repository navigation
Reverse Proxy
Browsers only allow microphone access on HTTPS pages (or localhost), so a public WebSpeak3 instance needs TLS. You can let the gateway do that itself (TLS_CERT / TLS_KEY), but usually it sits behind a reverse proxy that handles certificates.
Everything is served by the gateway on one port (default 8080):
| Path | What |
|---|---|
/ws |
The WebSocket the browser talks to. Must be proxied as a WebSocket (Upgrade / Connection headers). This is the most common mistake: the page loads, but connecting never works. |
/api/* |
Optional endpoints (broadcast, Design Store, feedback). |
/healthz |
Returns ok, for health checks. |
| everything else | The web client itself. |
The gateway sends a keepalive every 20 seconds, so default proxy timeouts are fine. A single WebSocket message can be up to 34 MiB (file transfers), which none of the proxies below limit by default.
Behind a proxy, every connection comes from the proxy's address, so the per-IP limit (MAX_CONNECTIONS_PER_IP) would treat all users as one. TRUST_PROXY tells the gateway where to find the real client IP:
| Setup | Setting | Client IP taken from |
|---|---|---|
| Your own proxy (nginx, Caddy, Traefik, …) | TRUST_PROXY=1 |
The last X-Forwarded-For entry, the one your proxy added. Earlier entries come from the browser and are ignored. |
| Cloudflare Tunnel, or Cloudflare's proxy in front (also in front of your own proxy) | TRUST_PROXY=cloudflare |
CF-Connecting-IP, which Cloudflare always sets itself. |
Only set it if the gateway can only be reached through the proxy (don't publish the gateway's port to the internet, or bind it to 127.0.0.1). Otherwise anyone can send these headers directly and pick their own "IP".
TRUST_PROXY=cloudflare exists since v0.16.0-beta.3. Older gateways read CF-Connecting-IP and the first X-Forwarded-For entry with TRUST_PROXY=1, which clients can fake behind a non-Cloudflare proxy, so update if you use one.
Also set ALLOWED_ORIGINS to your public address so other websites can't use your gateway.
server {
listen 443 ssl;
server_name voice.example.com;
ssl_certificate /etc/letsencrypt/live/voice.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/voice.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 1h;
}
}Use TRUST_PROXY=1 with this.
voice.example.com {
reverse_proxy 127.0.0.1:8080
}Caddy gets the certificate automatically and proxies WebSockets and X-Forwarded-For without extra settings. Use TRUST_PROXY=1.
services:
webspeak3:
image: moepchi/webspeak3:latest
restart: unless-stopped
environment:
- TRUST_PROXY=1
- ALLOWED_ORIGINS=https://voice.example.com
labels:
- traefik.enable=true
- traefik.http.routers.webspeak3.rule=Host(`voice.example.com`)
- traefik.http.routers.webspeak3.entrypoints=websecure
- traefik.http.routers.webspeak3.tls.certresolver=letsencrypt
- traefik.http.services.webspeak3.loadbalancer.server.port=8080Traefik handles WebSockets and X-Forwarded-For automatically.
The repository's docker-compose.yml already includes a cloudflared service. Point the tunnel's public hostname at http://webspeak3:8080, put the token in .env as CLOUDFLARE_TUNNEL_TOKEN, and keep TRUST_PROXY=cloudflare. Cloudflare sets CF-Connecting-IP itself, and the gateway publishes no port, so nothing else can reach it.
If the web client is hosted somewhere else (a static host or CDN) and only the gateway runs on your server:
- Build the client with
VITE_GATEWAY_URL=wss://gateway.example.com(the/wspath is added automatically). It must bewss://if the page is HTTPS. - Set
WEB_STATIC=0on the gateway if it shouldn't serve the client too. - Set
ALLOWED_ORIGINSto the client's address, e.g.https://client.example.com. - Features that rely on the gateway serving the page (runtime
DEFAULT_SERVER, theSERVER_PICKERdropdown) don't apply then. Use the build-time variables instead; see Configuration & Environment Variables.
-
https://voice.example.com/healthzshowsok. - The page loads and connecting to a server works. If the page loads but connecting hangs or fails right away, the
/wsWebSocket isn't being passed through. - The browser asks for microphone access. If it doesn't, the page isn't on HTTPS.