Skip to main content
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. 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.
1

Create a TURN key

In the Cloudflare dashboard, open Realtime > TURN Server and create a key. Copy the Turn Token ID and its API token (the token is shown only once).
2

Set the server environment

In .env:
.env
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.

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

Setup

1

Create the coturn config

Terminal
This file holds your shared secret, so it is never shipped and you always create it yourself.
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.
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 (both commented out) to the paths of your TLS certificate and key
  • external-ip to the machine’s public IP, if the interface itself holds a private address
external-ip is commented out in the example file, and uncommenting it is the normal case on AWS, GCP, Azure, and any VPS behind 1:1 NAT. Leave it commented out only when the public address is bound directly to the interface. Get it wrong and coturn advertises a relay candidate nobody can route to, so relayed transfers fail with no error message.The certificate mount is shipped commented out too. Uncomment it in docker-compose.yml so the paths you just set in turnserver.conf exist inside the container:
docker-compose.yml
If you started from the curl quickstart rather than a clone, that means editing your downloaded docker-compose.yml. Skip it and the signaling server still advertises a turns: URL, pointing at a port that cannot complete a TLS handshake.
2

Match the server environment

In .env:
.env
The signaling server uses these to issue time-limited HMAC-SHA1 credentials for coturn. Credentials expire after 24 hours.
3

Open firewall ports

On the host, open the following ports:
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, coturn keeps its own ports and its own TLS certificate.
4

Start the stack with the TURN profile

Terminal
Create coturn/turnserver.conf first, as the earlier step describes. Starting the profile without it looks successful and is not.

Verify

Check that the signaling server returns TURN credentials:
Terminal
When TURN is configured, the response contains turn: and turns: entries, plus a stun: entry pointing at your TURN host:
Output
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.
Relayed transfers stay capped at 2 GB per session even on a relay you run yourself. The cap is compiled into the browser client, the CLI, and the desktop app, not read from your server, so raising it means building all three from modified source. Direct connections are unaffected and have no size limit. See The 2 GB Relay Limit.