> ## 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.

# Add a TURN Relay to Self-Hosted Floe

> Add a coturn TURN relay to a self-hosted Floe instance so peers behind strict NAT, CGNAT, or corporate firewalls can still complete file transfers.

A TURN server relays the (still end-to-end encrypted) stream when two peers cannot reach each other directly. This is common with symmetric NAT or carrier-grade NAT. Without TURN, those specific transfers fail. All other transfers still work over direct connections.

You have two options: a managed TURN service (recommended) or a self-hosted coturn relay.

## Managed TURN via Cloudflare (recommended)

Cloudflare's Realtime TURN service runs on Cloudflare's global anycast network. It needs no public IP, no UDP port range, and no TLS certificates on your side, and it includes a generous free tier.

<Steps>
  <Step title="Create a TURN key">
    In the [Cloudflare dashboard](https://dash.cloudflare.com), open **Realtime > TURN Server** and create a key. Copy the **Turn Token ID** and its **API token** (the token is shown only once).
  </Step>

  <Step title="Set the server environment">
    In `.env`:

    ```env theme={null}
    CLOUDFLARE_TURN_KEY_ID=<your Turn Token ID>
    CLOUDFLARE_TURN_KEY_API_TOKEN=<your API token>
    ```

    The signaling server mints short-lived ICE credentials from Cloudflare and caches them. These variables take precedence over the coturn variables below, so you do not need to run coturn.
  </Step>
</Steps>

## Self-hosting with coturn

Prefer to run the relay yourself? Leave the Cloudflare variables unset and configure coturn instead.

TURN requires a **public IP address**, a **domain name**, and **TLS certificates**. The bundled `coturn` service uses host networking and is intended for a Linux host with a public IP.

<Warning>
  The coturn service uses host networking (`network_mode: host`). This only works on Linux. On macOS or Windows with Docker Desktop, you will need to run coturn separately or on a Linux VM.
</Warning>

## Setup

<Steps>
  <Step title="Create the coturn config">
    ```bash theme={null}
    curl -fsSL --create-dirs -o coturn/turnserver.conf \
      https://raw.githubusercontent.com/jannskiee/floe/main/coturn/turnserver.conf.example
    ```

    This file holds your shared secret, so it is never shipped and you always create it yourself.

    <Warning>
      Do not skip this. Docker creates a **directory** at that path instead, and coturn then **starts anyway** and reports healthy, running on built-in defaults with no auth secret. Relaying silently fails for exactly the peers TURN exists to serve. The giveaway is `Default realm: localdomain` in `docker compose logs coturn`.

      Recovering needs `rmdir coturn/turnserver.conf` **first**: copying onto a directory succeeds and writes the file inside it, so the obvious fix appears to work and does not.
    </Warning>

    Edit `coturn/turnserver.conf` and set:

    * `static-auth-secret` to a strong random value (e.g. `openssl rand -hex 32`)
    * `realm` to your TURN hostname (e.g. `turn.your-domain.com`)
    * `cert` and `pkey` to the paths of your TLS certificate and key

    See the volume mounts in `docker-compose.yml` for how to make certificates available inside the container.
  </Step>

  <Step title="Match the server environment">
    In `.env`:

    ```env theme={null}
    TURN_SECRET=<same value as static-auth-secret>
    TURN_DOMAIN=turn.your-domain.com
    ```

    The signaling server uses these to issue time-limited HMAC-SHA1 credentials for coturn. Credentials expire after 24 hours.
  </Step>

  <Step title="Open firewall ports">
    On the host, open the following ports:

    | Port          | Protocol | Purpose                                              |
    | ------------- | -------- | ---------------------------------------------------- |
    | `3478`        | UDP/TCP  | STUN and TURN                                        |
    | `5349`        | UDP/TCP  | TURNS (TURN over TLS)                                |
    | `49152-65535` | UDP      | Relay data range (configurable in `turnserver.conf`) |

    <Warning>
      These ports must be reachable directly. TURN is not HTTP, so it cannot be routed through the reverse proxy that fronts the web client and signaling server. Even on a [one-domain deployment](/docs/self-hosting/reverse-proxy), coturn keeps its own ports and its own TLS certificate.
    </Warning>
  </Step>

  <Step title="Start the stack with the TURN profile">
    ```bash theme={null}
    docker compose --profile turn up -d
    ```

    <Warning>
      Create `coturn/turnserver.conf` first, as the earlier step describes. Starting the profile without it looks successful and is not.
    </Warning>
  </Step>
</Steps>

## Verify

Check that the signaling server returns TURN credentials:

```bash theme={null}
curl http://localhost:3001/api/turn-credentials
```

When TURN is configured, the response contains `turn:` and `turns:` entries, plus a `stun:` entry pointing at your TURN host:

```json theme={null}
[
  { "urls": "stun:turn.your-domain.com:3478" },
  { "urls": "turn:turn.your-domain.com:3478", "username": "...", "credential": "..." },
  { "urls": "turns:turn.your-domain.com:5349", "username": "...", "credential": "..." }
]
```

With Cloudflare configured, the response instead contains `turn.cloudflare.com` entries. When neither the Cloudflare variables nor `TURN_SECRET` / `TURN_DOMAIN` are set, the endpoint returns the Google public STUN servers only.

## How credentials work

The signaling server generates time-limited credentials using HMAC-SHA1. The username is `{expiry_unix_timestamp}:floeuser` and the password is `base64(HMAC-SHA1(TURN_SECRET, username))`. coturn validates these against the shared secret without needing a database of user accounts.
