Skip to main content
This page deploys Floe on two hostnames: the web client on one and the signaling server on the other, each behind a reverse proxy with HTTPS. It is for an instance that people reach from other machines and that cannot share one domain.
Most instances are simpler behind one domain, with a single certificate and a single address for every client. See Run behind one domain for copy-pasteable Caddy and nginx configuration. The two-hostname layout on this page is still fully supported.

Steps

1

Put both services behind a reverse proxy with HTTPS

Use nginx, Caddy, Traefik, or a managed platform like Render or Fly, and proxy each service to a hostname you control:
  • app.your-domain.com to the client on :3000
  • api.your-domain.com to the signaling server on :3001
Serve both over HTTPS. Over plain HTTP at any address other than localhost, a browser runs the web app without the screen wake lock, the clipboard API, and service workers, so the screen can sleep during a long transfer.The signaling server uses WebSocket connections. Make sure your proxy forwards the Upgrade and Connection headers.The steps below set values in the .env file of a Docker Compose stack. On a managed platform, set the same values as each service’s own variables instead: SOCKET_URL on the client, and CLIENT_URL and TRUSTED_PROXY_COUNT on the server.
Keep the hostname in the Host header as the client sent it. Caddy and Traefik do by default, and nginx does with proxy_set_header Host $host. The CLI and the desktop app connect to api.your-domain.com, which is not your CLIENT_URL, so the server lets them in by matching their Origin against the hostname in Host. A proxy that rewrites Host to the upstream address (localhost:3001, the nginx default without that line) gets them refused with a 403. Origins the server accepts shows what each side reports when that happens.
2

Set NEXT_PUBLIC_SOCKET_URL

In .env:
.env
Nothing is rebuilt for this. Compose maps the value onto the client container’s SOCKET_URL, and the browser reads it from GET /api/config on every page load.
3

Set CLIENT_URL

In .env:
.env
This adds your client origin to the server’s allow-list. It is matched exactly, including the scheme and with no trailing slash. Without it, HTTP requests from the web app on your domain are refused with a 403, and its signaling connection is refused too.
4

Set TRUSTED_PROXY_COUNT

Set this to the number of proxy hops in front of the signaling server, so the per-address rate limits read real client addresses instead of the proxy’s:
.env
One reverse proxy is 1, a CDN in front of that proxy is 2. The server’s own default is 1; Docker Compose passes 0 explicitly, which is correct only when the container ports are published directly. Left at 0 behind a proxy, every user counts as the proxy’s address and shares one set of limits, and the room seal that keeps a third network out of a link stops working. Setting it higher than your real hop count lets clients forge X-Forwarded-For and bypass the limits. Keep ports 3000 and 3001 from being reached around the proxy as well: see TRUSTED_PROXY_COUNT.
5

Recreate the containers

Terminal
Compose applies a changed .env only when it recreates a container, so run this again after any later change to that file.
6

Point the CLI and the desktop app at your instance

A split deployment has two addresses, so both clients need both. The CLI takes them as flags, or as FLOE_SERVER and FLOE_WEB in the environment:
Terminal
Without --web, the share link points at the signaling API rather than the web app, because a self-hosted server is otherwise assumed to serve both from one origin.In the desktop app, open Settings and, under Advanced, expand Server. Fill in both Server address (https://api.your-domain.com) and Share link address (https://app.your-domain.com), then click Test. See Desktop app settings.Request links are built from Share link address as well, and they stay off until the server’s policy file turns them on. See Turn on request links.

Origins the server accepts

A browser is accepted only from an origin on the server’s allow-list. The list always holds the official floe.one origins (https://floe.one and https://www.floe.one) plus http://localhost:3000 for local development. Your CLIENT_URL is added on top of these, so pages served from your domain and from floe.one are both allowed. If you are running a private fork and want a strict allow-list, remove the hard-coded entries from server/server.js. Two more cases are accepted. A request with no Origin header gets through, which covers the plain HTTP calls the CLI and the desktop app make. A WebSocket whose Origin names the same hostname as the request’s Host header gets through too, which is how the CLI and the desktop app connect to api.your-domain.com. When Host carries a port other than the scheme’s default, the port has to match as well. That second rule is why the proxy has to pass Host through. A refused origin, or a proxy that does not forward /ws, looks different on each side: The CLI lines end in a hint from floe newer than 1.11.0; floe 1.11.0 and earlier stop after websocket: bad handshake. Floe Desktop 0.3.0 shows the /ws line, the second of its three, for every refusal. A browser shows no message of its own for a refused origin. The web app’s badge stays Offline.

If something goes wrong

Last modified on October 15, 2026