The short version
With a Cloudflare account, create a TURN key, put its two values in.env, and run
docker compose up -d. Running coturn yourself instead takes a Linux host with a public IP
address, a domain, a TLS certificate (unless you set TURN_TLS=off), and open UDP ports. Either way, check the result with a
transfer that may only use the 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. Cloudflare charges for relayed data beyond a free allowance, and its TURN FAQ has the current figures.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 The signaling server mints credentials from Cloudflare that last 24 hours, and caches them for 5 minutes. These variables take precedence over the coturn variables below, so you do not need to run coturn. Set both: with only one of the pair, the server skips Cloudflare and logs one line saying so. A server image of version 1.11.7 or earlier logs nothing about it.
.env:.env
3
Recreate the server
Terminal
Self-hosting with coturn
Prefer to run the relay yourself? Leave the Cloudflare variables unset and configure coturn instead.The bundled
coturn service needs a Linux host with a public IP address,
a domain name, and TLS certificates, unless you set TURN_TLS=off. It uses host networking (network_mode: host), which only
works on Linux. On macOS or Windows with Docker Desktop, run coturn separately or on a Linux VM.Set up coturn
1
Create the coturn config
Terminal
2
Set the secret, the realm, and the public address
Edit
coturn/turnserver.conf and set:static-auth-secretto a strong random value (for example, the output ofopenssl rand -hex 32)realmto your TURN hostname (for example,turn.your-domain.com), the same value the server gets asTURN_DOMAINin a later step. Give that hostname a DNS record that points at this machine.external-ipto 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.3
Add the TLS certificate
Copy your certificate and its key into a The certificate mount is shipped commented out too. Uncomment it in If you started from the
coturn/certs folder next to docker-compose.yml, as coturn/certs/fullchain.pem and coturn/certs/privkey.pem. In coturn/turnserver.conf, uncomment the two lines that name them:coturn/turnserver.conf
docker-compose.yml so those paths exist inside the container:docker-compose.yml
curl quickstart rather than a clone, that means editing your downloaded docker-compose.yml. No certificate? Leave both lines commented out, skip the mount, and set TURN_TLS=off in the next step. Skip this step without it and the signaling server still advertises a turns: URL, pointing at a port that cannot complete a TLS handshake.4
Match the server environment
In The signaling server uses these to issue time-limited HMAC-SHA1 credentials for coturn. Credentials expire after 24 hours. Set both: with only one of the pair, the server hands out no coturn entries and logs one line saying so. Give
.env:.env
TURN_DOMAIN as the hostname alone: with a scheme, a port or a path the coturn relay is off, and the server logs one line about that too. A server image of version 1.11.7 or earlier logs neither. Set TURN_TLS=off when coturn runs without a certificate: the server then hands out the stun: and turn: entries only.TURN_TLS needs a server image newer than 1.11.0 and a docker-compose.yml from 1.11.1 or later. An older copy of the file does not pass it to the server, so the turns: entry stays; download the current one first, as Update to the latest code describes.5
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.
6
Start the stack with the TURN profile
Terminal
coturn/turnserver.conf first, as the first step describes. Starting the profile without it looks successful and is not.Verify
Check that the signaling server returns TURN credentials:Terminal
turn: and turns: entries, plus a stun: entry pointing at your TURN host:
Output
TURN_TLS=off the turns: entry is left out, and the response holds the first two.
With Cloudflare configured the response is two entries rather than three, and urls is an array rather than a string. One holds stun:stun.cloudflare.com:3478 with no username or credential, and the other a turn: and a turns: URL with the credentials. See the API reference for the exact shape. When neither the Cloudflare variables nor TURN_SECRET / TURN_DOMAIN are set, the endpoint returns the Google public STUN servers only.
That check proves the server hands out credentials, not that the relay carries data. A coturn with a wrong external-ip, a closed UDP range, or a bad certificate passes it. To prove the relay end to end, run a transfer that may only use the relay, between two machines:
Terminal
Output
Output
- Both keys are read once at startup, so a changed key takes effect only after
docker compose up -d(a restart, without Docker). The server starts with an empty cache, so the new key is used at once. - If the Cloudflare API starts failing, including after a key is deleted, the server keeps serving the last good copy for up to about 23 hours after it was minted. It logs
Cloudflare TURN mint failedat most once every 5 minutes. Only then does it fall back to coturn, if that is configured, or to Google STUN. A broken key can look healthy for most of a day.
What still leaves your server
A relay does not keep every client away from Google. When a client cannot fetch connection details from your server, because the request is misrouted, rate limited, or the server is down, it falls back to two hard-coded Google STUN addresses rather than giving up. The web app, the CLI, and Floe Desktop all do this, and only a transfer that forces the relay stops instead. The web app’s/docs link sends visitors on to floe.one’s documentation. If your instance exists
so that no traffic leaves your control, plan for both.
How credentials work
For coturn, 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.
Your relay keeps the 2 GB cap
Running your own relay does not lift the 2 GB cap on a relayed transfer. 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. See File size limits.Operating a relay
Everything in this section is for managed Cloudflare TURN except the last part, which is for coturn.Set a budget alert
Cloudflare can email you when your spending crosses a dollar amount you set. In the Cloudflare dashboard, go to Manage Account > Billing > Billable Usage and select Create budget alert. Cloudflare’s budget alerts documentation has the details. Pay-as-you-go accounts already have a default $10 alert; change its amount there or create your own (Cloudflare changelog). Know its limits before you rely on it:- It is account-wide. It adds up the usage-based spending of all your Cloudflare products, not the relay alone.
- It is available on Pay-as-you-go accounts only.
- It is informational and does not cap spending. The relay keeps carrying traffic after the email arrives.
- It is not real time. Cloudflare processes usage once a day, so the email comes the day after your spending crosses the amount.
See usage per username
Cloudflare reports TURN usage per credential username, but only through its GraphQL analytics API. Its TURN analytics documentation covers the metrics, theusername dimension and filter, and the API token permission a query
needs.
A username stands for one issuance, not one person. The server mints Cloudflare credentials at most
once every 5 minutes, and every client that fetches them between two mints gets the same username.
See the relay credentials reference for how long each set
lasts and how the server caches it.
Revoke a credential that leaked once
When a single credential has leaked, for example in a pasted log or a screenshot, revoke its username:Terminal
<KEY_ID> and <API_TOKEN> with your CLOUDFLARE_TURN_KEY_ID and
CLOUDFLARE_TURN_KEY_API_TOKEN values, and <username> with the username of the leaked entry.
Cloudflare answers 204 when the credential is revoked; see its
revoke documentation.
Every client that fetched credentials while that username was current holds the same one, so
revoking it revokes theirs too. If /api/turn-credentials still returns the username you revoked,
restart the server (docker compose restart server) so it mints a new one at once, or wait up to 5
minutes for the next mint. A restart also forgets every room and room code and stops
any CLI or Floe Desktop sender still waiting for its receiver; see
Operations.
Rotate the key
Revoking handles a credential that leaked once. A leaked API token needs a new key, because revoking cannot touch it.1
Create a new TURN key
In the Cloudflare dashboard, the same way as in
the managed TURN setup.
2
Switch the server to it
Put its ID and token in
.env and run docker compose up -d (without Docker, restart the
server). The server starts minting with the new key at once. A plain docker compose restart
does not apply a changed .env.3
Delete the old key
Nothing more can be minted with it afterward.
If you run coturn
The compose file pins coturn tocoturn/coturn:4.15, so pulling the stack keeps you on that
version. Moving to a newer coturn means changing the image line of the coturn service.
The example file refuses to relay to private, loopback, link-local and other reserved addresses,
and to multicast (its denied-peer-ip and no-multicast-peers lines). A
coturn/turnserver.conf made from an older copy of the example lacks those lines: copy them in
from the current example.
If any device that uses the relay can be on the relay’s own private network (for example, the
relay runs at home and you send or receive from a device in the same home), relayed connections to
that device fail unless you uncomment the example’s allowed-peer-ip line and set it to that
network’s IPv4 range. That line also lets everyone who gets credentials from your server reach that
range through the relay, so prefer running the relay on a network of its own. Leave the line
commented out when no device that uses the relay is on its network.
If something goes wrong
- A relay is configured but transfers that need it still fail: see The TURN relay is not working.
- The coturn log says
Default realm: localdomain: see coturn logsDefault realm: localdomain. - Test in Floe Desktop answers “Connected, but this server has no relay: Hide my IP will not work, and some networks will not connect”: see The Test button says the server has no TURN relay.