> ## Documentation Index
> Fetch the complete documentation index at: https://www.floe.one/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Run Floe Behind One Domain

> Serve the Floe web client and signaling server from a single domain with a reverse proxy, including copy-pasteable Caddy and nginx configuration for WebSocket support.

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. Send these paths to the signaling server and everything
else to the client.

| Path            | Goes to        | Why                                                                   |
| --------------- | -------------- | --------------------------------------------------------------------- |
| `/socket.io/`   | server `:3001` | Browser signaling. WebSocket upgrade plus HTTP long-polling fallback. |
| `/ws`           | server `:3001` | CLI and desktop signaling. WebSocket upgrade.                         |
| `/api/`         | server `:3001` | Room codes, TURN credentials, stats.                                  |
| `/health`       | server `:3001` | Health probe.                                                         |
| everything else | client `:3000` | The web app, including `/_next/` assets.                              |

<Warning>
  `/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`.
</Warning>

## Proxy configuration

<Tabs>
  <Tab title="Caddy">
    Caddy handles HTTPS certificates and WebSocket upgrades automatically, so the
    whole config is a handful of lines.

    ```caddyfile theme={null}
    floe.example.com {
        # Signaling server: WebSockets, room codes, TURN credentials, health.
        handle /socket.io/* {
            reverse_proxy localhost:3001
        }
        handle /ws {
            reverse_proxy localhost:3001
        }
        handle /api/* {
            reverse_proxy localhost:3001
        }
        handle /health {
            reverse_proxy localhost:3001
        }

        # Everything else is the web client.
        handle {
            reverse_proxy localhost:3000
        }
    }
    ```
  </Tab>

  <Tab title="nginx">
    nginx needs the `Upgrade` and `Connection` headers set explicitly, or the
    WebSocket handshake fails and transfers never start.

    ```nginx theme={null}
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    server {
        listen 443 ssl;
        server_name floe.example.com;

        # Your TLS certificate goes here.
        # ssl_certificate     /etc/letsencrypt/live/floe.example.com/fullchain.pem;
        # ssl_certificate_key /etc/letsencrypt/live/floe.example.com/privkey.pem;

        # Signaling server.
        location ~ ^/(socket\.io/|ws$|api/|health$) {
            proxy_pass http://127.0.0.1:3001;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            # The server heartbeats every 30s. Keep idle CLI sockets alive.
            proxy_read_timeout 300s;
        }

        # Web client.
        location / {
            proxy_pass http://127.0.0.1:3000;
            proxy_http_version 1.1;
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    ```
  </Tab>
</Tabs>

## 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 theme={null}
NEXT_PUBLIC_SOCKET_URL=
NEXT_PUBLIC_SITE_URL=https://floe.example.com
CLIENT_URL=https://floe.example.com
TRUSTED_PROXY_COUNT=1
```

```bash theme={null}
docker compose up -d
```

`NEXT_PUBLIC_SITE_URL` is the only build-time value here. The published image
cannot carry it, so canonical links and share previews are omitted rather than
pointed somewhere wrong. See [Container Images](/docs/self-hosting/images) to build the
client with your own domain baked in.

<Note>
  `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.
</Note>

`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 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:

```bash theme={null}
floe send --server https://floe.example.com photo.jpg
```

The `--web` flag only exists for split deployments where the client lives on a
different hostname than the signaling server.

## 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](/docs/self-hosting/turn-relay)
for the port list and firewall rules.

## Verify it works

```bash theme={null}
# Health check through the proxy.
curl https://floe.example.com/health

# TURN credentials endpoint (returns a JSON array of ICE servers).
curl https://floe.example.com/api/turn-credentials
```

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](/docs/self-hosting/production).
