September 05, 2025

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 config
  • certs/ – 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

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