Skip to main content
Floe has two services, but your users do not need two addresses. Put both behind one reverse proxy and everything lives at https://floe.example.com. This is the recommended production layout. It means one certificate, one DNS record, one value to configure, and share links that work without any extra setup.

Route table

The client and the signaling server claim different paths, so they coexist on one origin with no conflicts. Route them like this. /api/config is the one exception to the /api/ rule, and it needs its own route in both configs below. Send it to the signaling server and it returns 404. With the empty socket URL this page recommends below, the browser still lands on its own origin, which is the right answer, so the mistake never shows. Any instance that sets a non-empty socket URL loses it silently.
/ws is matched by exact string equality on the signaling server. A trailing slash or a path prefix will not match, and the connection is dropped with no error. If you proxy from a subpath, strip the prefix so the server still sees exactly /ws.

Proxy configuration

Caddy handles HTTPS certificates and WebSocket upgrades automatically, so the whole config is a handful of lines.
Caddyfile

Floe configuration

Leave the socket URL empty. The browser then talks to whatever host it was loaded from, which behind this proxy is already the right answer.
.env
Terminal
NEXT_PUBLIC_SITE_URL is the only build-time value here, which is why the comment above tells you the running stack does not read it. The published image cannot carry it, so canonical links and share previews are omitted rather than pointed somewhere wrong. See Container Images to build the client with your own domain baked in.
CLIENT_URL is matched exactly, including the scheme and with no trailing slash. https://floe.example.com and https://www.floe.example.com are different origins to the CORS check, so use whichever one your users actually visit.
TRUSTED_PROXY_COUNT must equal the number of proxy hops in front of the signaling server. One reverse proxy means 1. A CDN in front of that proxy means 2. Setting it higher than your real hop count lets clients forge X-Forwarded-For and slip past the per-IP rate limits.

Using the CLI and the desktop app with a one-domain instance

One address is all the CLI needs. The share link it prints resolves to the same origin, so it opens the web client correctly with no extra flag:
Terminal
The --web flag only exists for split deployments where the client lives on a different hostname from the signaling server. The desktop app takes the same single address. Open Settings, expand Advanced, put https://floe.example.com in Server address, and press Test. The test runs three checks in order: that /health answers as a Floe signaling server, that the realtime /ws connection is accepted, and that GET /api/turn-credentials returns usable connection details. A proxy that forwards only some of those is named in the failure message instead of breaking silently later. Leave Share link address blank on a one-domain instance and the app derives the link from the server address. See Desktop Settings.
The test deliberately does not follow redirects, so a proxy that answers /health with a 301 is reported as “may be the web app rather than the signaling server” rather than passing.

TURN does not go through the proxy

If you run the optional coturn relay, its ports stay directly exposed. TURN is not HTTP, so a reverse proxy cannot carry it. See TURN Relay for the port list and firewall rules.

Verify it works

Terminal
Then open https://floe.example.com in a browser. The connection badge should read Ready. If it stays disconnected, the /socket.io/ route is the first thing to check.

Prefer two subdomains?

Splitting the client and server across app.your-domain.com and api.your-domain.com is still fully supported. See Production Deployment.