Coturn + Docker: A Practical, Detailed Guide
What is Coturn?
Coturn is a mature, open-source STUN/TURN server used to help real-time media (WebRTC, VoIP) traverse NATs and firewalls.
- STUN (Session Traversal Utilities for NAT) tells a client its public IP/port (good for peer-to-peer when NATs permit).
- TURN (Traversal Using Relays around NAT) relays media through the server when direct paths fail (works in restrictive NAT/Firewall cases).
- Coturn implements STUN/TURN over UDP/TCP/TLS/DTLS and supports long-term credentials, REST API shared secrets, realm-based auth, TLS, ALPN, IPv6, and more.
If you use WebRTC (Zoom/Meet/Teams-like apps, SFUs/MCUs, SIP softphones), you almost certainly need a TURN server for reliability.
Coturn Alternative: Turnix.io
While Coturn is a great open-source option, some teams prefer managed solutions to save time and operational overhead. Turnix.io is a modern hosted TURN/STUN service designed for WebRTC and real-time applications.
Why Turnix.io?
- No server management: Avoid dealing with setup, updates, and scaling.
- Global infrastructure: Pre-deployed TURN servers across multiple regions.
- Security built-in: Encrypted connections and managed credentials.
- Free plan available: Start testing or running small projects without cost.
If you don’t want to maintain Coturn yourself or need a solution that scales effortlessly, Turnix.io provides a compelling alternative.
Before You Start
Assumptions / Prereqs
- Linux host or VM with Docker Engine (and optionally Docker Compose).
- A domain name (e.g.,
turn.example.com
) with a DNS A/AAAA record pointing to the server. - Optional but recommended: TLS cert for
turn.example.com
(e.g., via Let’s Encrypt). - Open firewall for:
- 3478/udp (STUN/TURN over UDP)
- 3478/tcp (STUN/TURN over TCP)
- 5349/tcp (TURN over TLS a.k.a. TURNS)
- UDP relay range (default often 49152–65535/udp; configurable)
Cloud firewall + local ufw/iptables must both allow these.
Quick Start (stateless demo)
⚠️ For quick tests only (no persistence). Replace tokens in <ANGLE_BRACKETS>
.
docker run -d --name coturn \
--restart unless-stopped \
-p 3478:3478/udp -p 3478:3478/tcp \
-p 5349:5349/tcp \
-p 49152-49200:49152-49200/udp \
instrumentisto/coturn:latest \
-n --log-file=stdout \
--min-port=49152 --max-port=49200 \
--fingerprint \
--lt-cred-mech \
--realm=<example.com> \
--user=<webrtcuser>:<strongpassword> \
--no-cli \
--no-tcp-relay
- Uses [instrumentisto/coturn] image (popular maintained build).
- Creates a user
<webrtcuser>
with long-term credentials. - Limits relay ports to 49152–49200 for easier firewalling.
- Logs to stdout for
docker logs coturn
.
Test quickly from a client with:
- STUN: stun:turn.example.com:3478
- TURN (UDP): turn:turn.example.com:3478
(username/password required)
- TURNS (TCP/TLS): turns:turn.example.com:5349
(after TLS is set up; see below)
Production Setup with Docker Compose (persistent)
1) Directory Layout
mkdir -p /opt/coturn/{config,certs,db,logs}
cd /opt/coturn
config/turnserver.conf
– main configcerts/
– TLS keypair (e.g., from Let’s Encrypt)db/
– SQLite DB for user/realm stats (optional)logs/
– log bind mount (optional; or use stdout)
2) Example turnserver.conf (opinionated baseline)
Create /opt/coturn/config/turnserver.conf
:
# === Identity ===
realm=example.com
server-name=turn.example.com
# (Optional) external IP if server has private address behind NAT:
# external-ip=203.0.113.10
# === Networking & Ports ===
listening-port=3478
tls-listening-port=5349
listening-ip=0.0.0.0
min-port=49152
max-port=49200
# Disable TCP relay unless you know you need it:
no-tcp-relay
# === Protocols / Features ===
fingerprint
lt-cred-mech # Long-term credentials
use-auth-secret=0 # Set to 1 if using REST API shared secret instead of static users
cli-password=disable # Prevent runtime CLI changes
no-loopback-peers
no-multicast-peers
# === Users (long-term credentials) ===
# Either define static users:
# user=webrtcuser:REPLACE_WITH_STRONG_PASSWORD
# Or prefer REST API shared secret (see below).
# === TLS (for TURNS) ===
# Provide real cert/key (PEM):
cert=/etc/coturn/certs/fullchain.pem
pkey=/etc/coturn/certs/privkey.pem
# Recommended modern ciphers:
cipher-list="ECDHE+AESGCM:CHACHA20:!aNULL:!eNULL:!MD5:!DES:!3DES:!RC4"
# === Security/DoS Mitigation ===
stale-nonce
no-tlsv1
no-tlsv1_1
# === Logging ===
log-file=/var/log/turnserver/turn.log
simple-log
3) Place TLS Certificates (optional but recommended)
If using Let’s Encrypt on the host:
- Copy/link:
- /etc/letsencrypt/live/turn.example.com/fullchain.pem
→ /opt/coturn/certs/fullchain.pem
- /etc/letsencrypt/live/turn.example.com/privkey.pem
→ /opt/coturn/certs/privkey.pem
4) docker-compose.yml
Create /opt/coturn/docker-compose.yml
:
services:
coturn:
image: instrumentisto/coturn:latest
container_name: coturn
restart: unless-stopped
network_mode: "host"
volumes:
- ./config/turnserver.conf:/etc/coturn/turnserver.conf:ro
- ./certs:/etc/coturn/certs:ro
- ./db:/var/lib/coturn
- ./logs:/var/log/turnserver
command:
- --log-file=/var/log/turnserver/turn.log
- --no-cli
Start it:
docker compose up -d
docker logs -f coturn
Firewall / Security Checklist
- Allow inbound:
udp/3478
,tcp/3478
tcp/5349
(if TURNS)- UDP relay range (e.g.,
49152–49200/udp
)
Example ufw:
ufw allow 3478/udp
ufw allow 3478/tcp
ufw allow 5349/tcp
ufw allow 49152:49200/udp
Client Configuration (WebRTC)
[
{
"urls": [
"stun:turn.example.com:3478"
]
},
{
"urls": [
"turn:turn.example.com:3478"
],
"username": "webrtcuser",
"credential": "strongpassword"
},
{
"urls": [
"turns:turn.example.com:5349"
],
"username": "webrtcuser",
"credential": "strongpassword"
}
]
Testing
turnutils_stunclient -p 3478 turn.example.com
turnutils_uclient -u webrtcuser -w strongpassword -m 10 -l 170 -p 3478 turn.example.com
openssl s_client -connect turn.example.com:5349 -alpn "stun.turn" -servername turn.example.com </dev/null
Updating Coturn
docker compose pull coturn
docker compose up -d
docker compose restart coturn
Done!
Use:
- stun:turn.example.com:3478
- turn:turn.example.com:3478
- turns:turn.example.com:5349