Who this is for
Your Jitsi server works for most people, but some users get no audio or video. Typical reports: it works at home but not at the office, on hotel Wi-Fi or behind a VPN, or a call works with two people and breaks when a third joins. You want those users to connect through a relay without opening anything on their side.
How it works
Media paths. With two participants, Jitsi tries a direct peer-to-peer (P2P) connection. From three participants, everyone sends media to the videobridge (JVB) on UDP 10000. Browsers find paths with ICE, which gathers three kinds of candidate: host (local address), srflx (public address learned from STUN) and relay (an address on a TURN server).
Who needs TURN. TURN is the fallback when no direct path works:
- Networks that block outbound UDP, so UDP 10000 is unreachable. The Debian install calls TCP 5349 the fallback “when UDP is blocked”.
- Networks that only allow TCP 443.
- P2P calls where both people sit behind NATs with “address-dependent mapping” (often called symmetric NAT), where hole punching generally fails. A Jitsi maintainer put it as “STUN alone is not enough”.
How browsers get credentials. The browser asks Prosody for STUN and TURN servers over XMPP (XEP-0215, urn:xmpp:extdisco:2). Prosody’s mod_external_services answers with short-lived credentials derived from a shared secret (the TURN REST scheme). coturn checks them with use-auth-secret and static-auth-secret. The old mod_turncredentials module was removed in jitsi-meet PR #17453, first shipped in 2.0.11031.
What TURN is used for. P2P connections get every STUN and TURN server. The JVB connection only gets TURN over TCP or TLS. TURN over UDP is filtered out unless useTurnUdp is set, because the bridge is normally reachable on UDP. coturn then forwards the media to the bridge over UDP. A maintainer: “the turnserver should be able to communicate with JVB over UDP on its public address”. The videobridge itself dropped ICE over TCP in PR #2194 (2024-07-18), so TURN is the only TCP path left.
What ships. On Debian, jitsi-meet recommends jitsi-meet-turnserver, which installs coturn and configures it for UDP 3478 and TLS 5349. docker-jitsi-meet does not include a TURN server.
Before you start
- A public IP for coturn, and a DNS name for it. These steps use
meet.example.comfor both Jitsi and TURN on ports 3478/5349. To share port 443 with Jitsi on one IP, you need a second name such asturn.meet.example.com. - A trusted TLS certificate for the TURN name.
- Firewall rules:
3478/udp,5349/tcp, and the relay range,49152-65535/udpby default. - A shared secret, for example
openssl rand -hex 32(the Docker password script usesopenssl rand -hex 16the same way). - docker-jitsi-meet stable-11248 or Debian packages 2.0.11248.
Steps
Docker (docker-jitsi-meet): coturn on the same host, ports 3478 and 5349
-
Install coturn on the host. The Jitsi coturn template is written for the distribution package:
Terminalsudo apt install coturn sudo sed -i 's/#TURNSERVER_ENABLED/TURNSERVER_ENABLED/' /etc/default/coturnThe second line enables the service, the same way the Jitsi installer does. The official
coturn/coturnimage also works. Its README recommends--network=hostbecause Docker handles large port ranges badly. -
Write
/etc/turnserver.conf. This follows the Jitsi template with explicit relay ports:Configuse-auth-secret static-auth-secret=REPLACE_WITH_SHARED_SECRET realm=meet.example.com listening-port=3478 tls-listening-port=5349 min-port=49152 max-port=65535 cert=/etc/coturn/certs/meet.example.com.fullchain.pem pkey=/etc/coturn/certs/meet.example.com.privkey.pem keep-address-family no-multicast-peers no-cli no-tcp-relay no-tcp no-dtls no-tlsv1 no-tlsv1_1 syslogThen copy every
denied-peer-ip=line from the Jitsi template. Those lines stop the relay from reaching private and reserved networks. If the server is behind 1:1 NAT (for example AWS), addexternal-ip=<public-ip>/<private-ip>. -
Give coturn the certificate. If Jitsi’s web container gets Let’s Encrypt for
meet.example.com, the files arefullchain.pemandkey.pemin${CONFIG}/storage/web/acme-certs/meet.example.com/. Copy them in the same way Jitsi’s own hook does:Terminalsudo mkdir -p /etc/coturn/certs sudo cp ~/.jitsi-meet-cfg/storage/web/acme-certs/meet.example.com/fullchain.pem /etc/coturn/certs/meet.example.com.fullchain.pem sudo cp ~/.jitsi-meet-cfg/storage/web/acme-certs/meet.example.com/key.pem /etc/coturn/certs/meet.example.com.privkey.pem sudo chown turnserver /etc/coturn/certs/*.pem sudo chmod 400 /etc/coturn/certs/*.pem sudo systemctl restart coturnRepeat after each renewal, or schedule it.
-
Open the ports:
Terminalsudo ufw allow 3478/udp sudo ufw allow 5349/tcp sudo ufw allow 49152:65535/udp -
Point Prosody at coturn in
.env. These are the variables the stable-11248 Prosody template reads:ConfigTURN_CREDENTIALS=REPLACE_WITH_SHARED_SECRET TURNS_HOST=meet.example.com TURNS_PORT=5349 TURN_HOST=meet.example.com TURN_PORT=3478 TURN_TRANSPORT=udpSet the ports explicitly.
TURN_PORTandTURNS_PORTdefault to443, andTURN_TRANSPORTdefaults totcp.TURN_CREDENTIALSmust equal coturn’sstatic-auth-secret. -
Recreate Prosody:
Terminaldocker compose up -d prosody
Docker: TURN on port 443
Some networks only allow TCP 443. You have two choices:
- Separate host or second IP (simplest). Run coturn there with
tls-listening-port=443, then setTURNS_HOST=turn.meet.example.comandTURNS_PORT=443. Jitsi’s own service advertisesmeet-jit-si-turnrelay.jitsi.net:443. The upstream coturn service runs as userturnserver. If it cannot bind 443, see Open questions. - Same host as Jitsi. One IP can serve both only through SNI multiplexing. The handbook shows this for packages with an nginx
streamblock that sendsturn.meet.example.comto coturn on 5349 and the meet name to the web server. For Docker, the same block would sit in a host nginx in front of the web container. That adaptation is not in the handbook (see Open questions).
Debian/Ubuntu packages
-
Check that TURN is already set up.
jitsi-meetrecommendsjitsi-meet-turnserver:Terminaldpkg -l jitsi-meet-turnserver coturn head -1 /etc/turnserver.conf grep -n -A4 'external_services = {' /etc/prosody/conf.d/meet.example.com.cfg.luaThe first line of the coturn config should be
# jitsi-meet coturn config. Do not modify this line. -
Install it if it is missing:
Terminalsudo apt install jitsi-meet-turnserverThe installer moves any non-Jitsi
/etc/turnserver.confto/etc/turnserver.conf.bak. It fills in your domain and secret, and enables coturn in/etc/default/coturn. If a Jitsi coturn config already exists, it printsturnserver is already configured on this machine.and leaves it. -
Open the ports:
3478/udpand5349/tcp, plus the relay range. -
Replace old
turncredentialssetups. Configs from old installs that still loadturncredentialsneed theexternal_servicesblock from the current template. A maintainer said: “Do not use turn_external use external_services”. -
For port 443 on the same host, follow the handbook: a second DNS name, the nginx
streamblock, nginx moved to port 4444, and the Prosodyturnsentry changed to port 443.
Configuration reference
| Name | Where | Default | What it does |
|---|---|---|---|
TURN_CREDENTIALS |
Docker .env |
unset | Becomes external_service_secret. Must match coturn static-auth-secret |
TURN_HOST / TURNS_HOST |
Docker .env |
unset | TURN hosts, comma separated. Setting either loads external_services |
TURN_PORT / TURNS_PORT |
Docker .env |
443 / 443 |
Ports advertised to clients |
TURN_TRANSPORT |
Docker .env |
tcp |
udp, tcp or both, for turn entries |
TURN_TTL |
Docker .env |
86400 |
Credential lifetime in seconds. The handbook table misspells it TURN_TLL |
TURN_USERNAME / TURN_PASSWORD |
Docker .env |
unset | Static credentials instead of a secret |
STUN_HOST / STUN_PORT |
Docker .env |
unset / 443 |
Adds a stun entry |
P2P_STUN_SERVERS |
Docker .env |
unset | Sets config.p2p.stunServers in the web config |
external_service_secret, external_services |
Prosody config | per template | Secret and server list sent to clients |
useTurnUdp |
config.js |
false |
Also use TURN/UDP for the JVB connection |
forceTurnRelay |
config.js |
unset | Relay-only ICE, for testing |
use-auth-secret, static-auth-secret |
/etc/turnserver.conf |
off | TURN REST credentials |
realm |
/etc/turnserver.conf |
unset | Realm, set to the domain in the Jitsi template |
listening-port / tls-listening-port |
/etc/turnserver.conf |
3478 / 5349 |
Plain and TLS listeners |
min-port / max-port |
/etc/turnserver.conf |
49152 / 65535 |
UDP relay range |
external-ip |
/etc/turnserver.conf |
empty | Public address when coturn is behind NAT |
denied-peer-ip |
/etc/turnserver.conf |
none | Ranges the relay must not reach |
no-tcp-relay, no-multicast-peers, no-cli |
/etc/turnserver.conf |
off | Hardening used by the Jitsi template |
Common mistakes
- Mixing auth modes.
TURN_CREDENTIALSis a shared secret foruse-auth-secret. It is not auser=name:passwordentry forlt-cred-mech. Use one mode: secret plusTURN_CREDENTIALS, or static users plusTURN_USERNAME/TURN_PASSWORD. A 2026 community guide mixes them. - Variables that do not exist.
ENABLE_TURNS,JVB_TURN_*andENABLE_TCP_HARVESTERare not in docker-jitsi-meet. A maintainer aboutJVB_TURN_*: “Those do not exist”. - Ports left at their defaults. Without
TURNS_PORT, clients are told port 443 while coturn listens on 5349. - Only
STUN_HOSTset.external_servicesonly loads whenTURN_HOSTorTURNS_HOSTis set, so the console showsservice-unavailableforget STUN/TURN credentials (extdisco:2). A maintainer called it harmless when you have no TURN server. Issue #2324 (P2P without TURN outside the LAN) is open as of 2026-10-05. - Relay blocked from the bridge. If coturn reaches the bridge on a private IP, a
denied-peer-iprange such as10.0.0.0-10.255.255.255blocks it. Let coturn reach the JVB public address. - Untrusted certificate on TURNS. coturn needs a trusted certificate for TLS.
- Relay range closed in the cloud firewall.
Service missing recommended 'transport' fieldin the Prosody log. Addtransport = "udp"to thestunentry. Harmless either way.Bad configuration format: no-loopback-peersordh2066from coturn. Current coturn no longer has these options. Loopback peers are blocked by default, so the warning is harmless.- One-way audio for VPN users is not always TURN. Clients with two network paths could get one-way audio from a bridge bug. It was fixed in jitsi-videobridge PR #2441, which ships in the 2.0.11248 packages (jvb
2.3-318-gbf271b11f).
Verify
On the TURN host:
sudo ss -lntup | grep turnserver
openssl s_client -connect meet.example.com:5349 -servername meet.example.com </dev/null 2>/dev/null | grep 'Verify return code'Expect listeners on :3478 (udp) and :5349 (tcp), and Verify return code: 0 (ok).
Check that Prosody advertises it (Docker renders config into /run):
docker compose exec prosody grep -A8 'external_services = {' /run/prosody/config/prosody.cfg.luaTest coturn directly. Make a one-hour credential from the secret, in the format coturn documents (timestamp:userid, base64 HMAC):
SECRET='REPLACE_WITH_SHARED_SECRET'
U="$(( $(date +%s) + 3600 )):test"
P=$(printf '%s' "$U" | openssl dgst -binary -sha1 -hmac "$SECRET" | base64)
echo "username: $U"; echo "password: $P"Open the WebRTC Trickle ICE page, add turns:meet.example.com:5349 with that username and password, set IceTransports to relay and gather. The page says a TURN server works “if you can gather a candidate with type relay”.
In a real call, add config.forceTurnRelay = true; to custom-config.js for a test. Join three people and open chrome://webrtc-internals (Firefox: about:webrtc). The active candidate pair’s local candidate should be type relay. Remove the setting afterwards.
If it still fails
- coturn logs to syslog in the Jitsi template:
sudo journalctl -u coturn -f. Watch for allocation and authentication errors while a client connects. - Browser console:
getting turn credentials failedoris mod_turncredentials or similar installed and configured?means Prosody did not answer extdisco. - No
relayin Trickle ICE: check the secret matches, the clock is correct (credentials expire), the certificate, and the firewall. relayappears but group calls still fail: coturn cannot reach the JVB over UDP on its public address.- A TCP-only load balancer in front of the JVB cannot work. Maintainers expect a public IP and port per bridge.
What we have not confirmed yet
We checked everything above against the Jitsi templates and source, the handbook, coturn and Prosody documentation, and maintainer comments on 2026-10-05. We have not yet tested every variant on a real server. These points are still open:
- Binding coturn to 443 as the
turnserveruser (packages) ornobody(Docker image with--network=host) may needCAP_NET_BIND_SERVICE. Neither the coturn docs nor Jitsi docs cover it. Test before recommending a systemd drop-in. - Same-host 443 for Docker: the handbook nginx
streamblock is written for packages. Running it in a host nginx in front of the Docker web container is our adaptation and is untested. - Whether guests on a secure domain setup get TURN credentials. On Debian,
external_servicesis enabled on the main VirtualHost only, and the secure domain guide does not mention it. - Exact Prosody error when an old config still loads
turncredentialson 2.0.11031 or newer. - Whether a stateful host firewall needs the full relay range open inbound, given the Debian quickstart does not list it.
- The handbook 443 section uses
/opt/acmesh/.acme.sh/acme.sh, a path from older installers (section last changed 2023). Current installs may differ. - The quickstart says turnserver setup is skipped when nginx already uses 443, but the current
jitsi-meet-web-configpostinst has no TURN logic. Possibly stale. - Issue #2324 says
P2P_STUN_SERVERShad no effect for the reporter. Not reproduced. - Which coturn release first rejected
no-loopback-peersanddh2066, and whether Debian 12 and Ubuntu 24.04 coturn builds warn. - Whether the Docker stable-11248 jvb image has the same jvb build as the 2.0.11248 package.
- The Jitsi template denies
203.0.113.0/24, the documentation range used for placeholders here. Real public IPs are unaffected.
Sources
- Jitsi handbook, Setting up TURN, last changed 2024-09-23, official doc
- jitsi-meet doc/debian/jitsi-meet-turn/turnserver.conf, checked 2026-10-05, source code
- jitsi-meet doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example, checked 2026-10-05, source code
- jitsi-meet debian/jitsi-meet-turnserver.postinst, checked 2026-10-05, source code
- jitsi-meet resources/coturn-le-update.sh, checked 2026-10-05, source code
- jitsi-meet config.js (p2p, useTurnUdp, forceTurnRelay), checked 2026-10-05, source code
- docker-jitsi-meet prosody/rootfs/defaults/prosody.cfg.lua at stable-11248, source code
- lib-jitsi-meet modules/xmpp/strophe.jingle.js (getStunAndTurnCredentials, TURN filter), checked 2026-10-05, source code
- docker-jitsi-meet docker-compose.yml and env.example at stable-11248, 2026-09-14, source code
- Prosody documentation, mod_external_services, checked 2026-10-05, official doc (Prosody)
- draft-uberti-behave-turn-rest-00, A REST API for access to TURN services, standards draft
- coturn examples/etc/turnserver.conf, coturn 4.18.0 released 2026-09-08, official doc (coturn)
- docker-jitsi-meet issue #2324 P2P does not work outside of LAN by default, opened 2026-09-11, open on 2026-10-05, maintainer comment (saghul) and community report
- Jitsi handbook, Self-Hosting Guide: Debian/Ubuntu server (firewall ports, TURN note), official doc
- Jitsi handbook, Self-Hosting Guide: Docker (TURN server configuration table, gen-passwords.sh), source last changed 2026-10-02, official doc
- coturn Docker image README, checked 2026-10-05, official doc (coturn)
- jitsi-meet PR #17453 chore(prosody): Remove deprecated modules (removes mod_turncredentials.lua), merged 2026-06-03, maintainer PR
- jitsi-videobridge issue #619 Using TCP instead of UDP, maintainer comments (damencho, 2021-05-06 and 2024-10-16)
- jitsi-videobridge commit 89dde6ea7 feat: Remove support for ICE/TCP (#2194), 2024-07-18, source code
- docker-jitsi-meet issues #2149 and #2123, and https://github.com/jitsi/docker-jitsi-meet/issues/2123, 2025, maintainer comments (damencho, aaronkvanmeerten)
- W3C WebRTC 1.0, RTCIceCandidateType enum (host, srflx, prflx, relay), official spec
- WebRTC samples, Trickle ICE, checked 2026-10-05, official tool (WebRTC project)
- RFC 8656, TURN: Relay Extensions to STUN, section 1, 2020-02, standard
- jitsi-meet issue #15943 Service missing recommended ‘transport’ field, 2025-04-17, maintainer comments (saghul, damencho)
- docker-jitsi-meet web/rootfs/defaults/settings-config.js at stable-11248, source code
- PiStack, Complete Guide to Self-Hosted Jitsi Meet 2026, 2026, community report (contains outdated variables)
- Jitsi stable apt repository Packages index (jitsi-meet 2.0.11248-1 Recommends jitsi-meet-turnserver, Pre-Depends jitsi-videobridge2 2.3-318-gbf271b11f-1), last modified 2026-09-14, official package metadata
- coturn examples/etc/coturn.service, source code
- docker-jitsi-meet web/rootfs/etc/s6-overlay/scripts/config at stable-11248 (acme-certs paths), source code
- docker-jitsi-meet issue #2224 service-unavailable error for extdisco:2, 2026-02-17, maintainer comment (saghul)
- coturn src/apps/relay/mainrelay.c (option table, “Bad configuration format”), checked 2026-10-05, source code
- jitsi-videobridge issue #2427 RTP sent to a non-nominated ICE pair, fixed by PR #2441 (2026-09-03), maintainer comment (JonathanLennox)
- Jitsi handbook, Docker guide, Jitsi Meet configuration (custom-config.js appended to config.js) and rendered config under /run, official doc
- Big Iron, Self-hosted conference bridge in 2026, 2026-06-11, community report (background only: TURN over TCP 443 is a slower fallback, fix UDP first)
Need a hand?
If some of your users still cannot connect, contact our engineers with a chrome://webrtc-internals dump from an affected user and the output of sudo journalctl -u coturn --since '10 min ago'. Our support plans include TURN setup and testing.