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

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

Match the server environment

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

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:
When TURN is configured, the response contains turn: and turns: entries, plus a stun: entry pointing at your TURN host:
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.