How to install Coturn TURN/STUN Server Installation on Debian 13
This guide installs and configures Coturn on Debian 13 (Trixie) for WebRTC/STUN/TURN workloads.
It covers two common network layouts:
1. Server has a public IP directly assigned
2. Server has only a private IP and sits behind NAT
Coturn is a TURN/STUN server commonly used for WebRTC media relay and NAT traversal. (GitHub)
System Requirements
Operating System
Debian 13 (Trixie)
Recommended minimum for a small TURN server:
CPU: 2 vCPU
Memory: 1-2 GB
Disk: 10 GB+
Network: Good bandwidth and low latency
TURN traffic is relayed through the server, so network bandwidth is usually much more important than CPU or disk.
For example, if a WebRTC call sends:
Client A → TURN → Client B
Client B → TURN → Client A
the TURN server handles traffic in both directions.
Network Requirements
Default TURN/STUN port:
3478 UDP
3478 TCP
Optional secure TURN:
5349 TCP TURN over TLS
5349 UDP TURN over DTLS
TURN also needs a relay UDP port range.
For example:
49152-65535 UDP
You can configure a smaller range if required:
49160-49260 UDP
However, reducing the relay range also reduces the number of simultaneous TURN allocations the server can support.
Coturn supports configuring relay allocation ports using min-port and max-port. (GitHub)
Network Scenario 1 — Server Has Public IP
Example:
Internet
│
│
▼
┌─────────────────────┐
│ Coturn Server │
│ │
│ 172.34.151.121 │
│ Public IP directly │
└─────────────────────┘
The public IP is directly configured on the Coturn server's network interface.
Check:
ip addr
Example:
inet 172.34.151.121/24
In this scenario, you normally do not need external-ip NAT mapping.
Network Scenario 2 — Server Has No Public IP Assigned
Example:
Internet
│
│
Public IP
172.34.151.121
│
▼
┌─────────────┐
│ Router/NAT │
└──────┬──────┘
│
│
10.10.10.20
│
▼
┌─────────────┐
│ Coturn │
│ Server │
└─────────────┘
The Coturn machine itself only has:
10.10.10.20
but the router/firewall owns:
172.34.151.121
Coturn supports this arrangement through an external/public-to-private address mapping. The official configuration documents the form:
external-ip=PUBLIC_IP/PRIVATE_IP
(GitHub)
For example:
external-ip=172.34.151.121/10.10.10.20
The NAT device must forward TURN and relay ports to the Coturn server without changing the relay port numbers. Coturn's configuration explicitly requires direct port mapping for this NAT scenario. (GitHub)
Important: "No Public IP" Can Mean Two Different Things
Case A — Coturn Machine Has No Public IP, But Router Does
This is supported.
Example:
Coturn:
10.10.10.20
Router:
172.34.151.121
Use:
external-ip=172.34.151.121/10.10.10.20
and configure port forwarding.
Case B — There Is No Publicly Reachable IP Anywhere
Example:
Internet
│
▼
ISP CGNAT
│
▼
100.64.x.x
│
▼
Router
│
▼
10.10.10.20
If your ISP uses CGNAT and you cannot forward ports from a public IP to the TURN server, a normal self-hosted Internet TURN server cannot be directly reached by external WebRTC clients.
In that situation you need something publicly reachable, for example:
Public VPS
Cloud VM
Dedicated server
Cloud TURN provider
Public IP from ISP
A TURN server's purpose is to provide a reachable relay for clients, so it must ultimately have a publicly reachable endpoint.
1. Install Coturn
Update Debian:
sudo apt update
Install:
sudo apt install -y coturn
Debian 13 provides the Coturn package with the systemd service, turnserver, turnadmin, /etc/turnserver.conf, and related TURN utilities. (Debian Packages)
Verify:
turnserver --version
2. Important Files
Main configuration:
/etc/turnserver.conf
Service defaults:
/etc/default/coturn
Systemd service:
coturn.service
Debian 13's Coturn package provides all of these files. (Debian Packages)
3. Enable Coturn
Enable at boot:
sudo systemctl enable coturn
Start:
sudo systemctl start coturn
Check:
sudo systemctl status coturn
Configuration — Public IP Directly Assigned
Assume:
TURN Domain: turn.example.com
Public IP: 172.34.151.121
Edit:
sudo nano /etc/turnserver.conf
Use:
# =============================================
# Network
# =============================================
listening-port=3478
listening-ip=172.34.151.121
relay-ip=172.34.151.121
min-port=49152
max-port=65535
# =============================================
# Authentication
# =============================================
fingerprint
lt-cred-mech
realm=turn.example.com
user=turnuser:CHANGE_THIS_PASSWORD
# =============================================
# Security
# =============================================
no-multicast-peers
# =============================================
# Logging
# =============================================
verbose
Because:
172.34.151.121
is directly assigned to the server, do not add external-ip merely because the address is public.
Configuration — Server Behind NAT
Assume:
Public IP: 172.34.151.121
Coturn Private: 10.10.10.20
Domain: turn.example.com
Edit:
sudo nano /etc/turnserver.conf
Configure:
# =============================================
# Network
# =============================================
listening-port=3478
listening-ip=10.10.10.20
relay-ip=10.10.10.20
external-ip=172.34.151.121/10.10.10.20
min-port=49152
max-port=65535
# =============================================
# Authentication
# =============================================
fingerprint
lt-cred-mech
realm=turn.example.com
user=turnuser:CHANGE_THIS_PASSWORD
# =============================================
# Security
# =============================================
no-multicast-peers
# =============================================
# Logging
# =============================================
verbose
The important difference is:
external-ip=172.34.151.121/10.10.10.20
Coturn uses this mapping when advertising relay addresses to clients behind NAT. (GitHub)
Router / NAT Port Forwarding
When Coturn is behind NAT, configure the router/firewall.
Forward:
Public Coturn
────────────────────────────────────────────
3478 UDP ───────────────▶ 10.10.10.20:3478
3478 TCP ───────────────▶ 10.10.10.20:3478
49152 UDP ───────────────▶ 10.10.10.20:49152
...
65535 UDP ───────────────▶ 10.10.10.20:65535
In shorthand:
UDP 3478 → 10.10.10.20:3478
TCP 3478 → 10.10.10.20:3478
UDP 49152-65535 → 10.10.10.20:49152-65535
The relay port must keep the same external port number.
For example:
Public UDP 50000
↓
Private UDP 50000
Good.
Avoid NAT rules like:
Public UDP 50000
↓
Private UDP 51000
Coturn's official configuration specifically notes that when external-ip is used behind NAT, relay ports must map directly to the corresponding external ports. (GitHub)
Smaller Relay Port Range
Opening:
49152-65535
gives Coturn a large allocation range.
For a small environment you may intentionally restrict it:
min-port=49160
max-port=49260
Then you only need:
UDP 49160-49260
on the firewall/NAT.
For example:
3478 TCP
3478 UDP
49160-49260 UDP
For a production server with significant call volume, use a larger range.
UFW Configuration
If UFW is enabled:
sudo ufw allow 3478/tcp
sudo ufw allow 3478/udp
sudo ufw allow 49152:65535/udp
Check:
sudo ufw status
Expected rules include:
3478/tcp ALLOW
3478/udp ALLOW
49152:65535/udp ALLOW
Restart Coturn
After changing configuration:
sudo systemctl restart coturn
Check:
sudo systemctl status coturn
Check Listening Ports
sudo ss -lntup | grep turnserver
Or:
sudo ss -lntup | grep 3478
You should see UDP/TCP listeners on:
3478
View Logs
sudo journalctl -u coturn
Follow logs:
sudo journalctl -u coturn -f
Last 100 lines:
sudo journalctl -u coturn -n 100
Authentication
Do not operate a public TURN server without authentication.
Coturn supports long-term credentials and TURN REST API authentication. Its documentation states that realm is used with the long-term credential mechanism or TURN REST API. (GitHub)
For a simple static user:
lt-cred-mech
realm=turn.example.com
user=turnuser:YOUR_STRONG_PASSWORD
WebRTC configuration:
{
urls: "turn:turn.example.com:3478",
username: "turnuser",
credential: "YOUR_STRONG_PASSWORD"
}
Recommended: TURN REST API Authentication
For an application such as an ASP.NET Core WebRTC service, I prefer temporary TURN credentials instead of a permanent username/password.
Configure Coturn:
use-auth-secret
static-auth-secret=YOUR_LONG_RANDOM_SECRET
realm=turn.example.com
fingerprint
Do not simultaneously use the static user=... configuration for the same authentication model.
Coturn provides TURN REST API/shared-secret support for generating time-limited credentials. (Debian Packages)
Generate a Secret
For example:
openssl rand -hex 32
Example:
5c2a73767ba3d41f39d107a60d61f7fb5360fba80729a414492a433535a22ca3
Configure:
use-auth-secret
static-auth-secret=5c2a73767ba3d41f39d107a60d61f7fb5360fba80729a414492a433535a22ca3
realm=turn.example.com
Keep this secret private.
TURN REST API Credential Generation
A temporary username generally uses an expiration timestamp:
EXPIRY_TIMESTAMP:USER_ID
For example:
1786940000:38
The credential is generated from the username and shared secret using HMAC-SHA1 and Base64.
Conceptually:
username =
expirationUnixTimestamp + ":" + userId
credential =
Base64(
HMAC-SHA1(
sharedSecret,
username
)
)
Then the browser receives:
{
urls: [
"turn:turn.example.com:3478?transport=udp",
"turn:turn.example.com:3478?transport=tcp"
],
username: "<temporary username>",
credential: "<temporary credential>"
}
This is preferable for browser applications because the permanent TURN secret never needs to be sent to the browser.
Recommended WebRTC Configuration
const config = {
iceServers: [
{
urls: [
"stun:turn.example.com:3478",
"turn:turn.example.com:3478?transport=udp",
"turn:turn.example.com:3478?transport=tcp"
],
username: username,
credential: credential
}
]
};
const pc = new RTCPeerConnection(config);
Normally WebRTC should try direct connectivity first and fall back to TURN when necessary.
Force TURN for Testing
To verify that Coturn actually works, temporarily configure:
const pc = new RTCPeerConnection({
iceTransportPolicy: "relay",
iceServers: [
{
urls: [
"turn:turn.example.com:3478?transport=udp",
"turn:turn.example.com:3478?transport=tcp"
],
username: username,
credential: credential
}
]
});
The important setting is:
iceTransportPolicy: "relay"
This forces WebRTC to use TURN relay candidates rather than silently succeeding through host or STUN server-reflexive candidates.
Verify Relay Candidate
In the browser:
pc.onicecandidate = e => {
if (e.candidate) {
console.log(e.candidate.candidate);
}
};
A successful Coturn allocation should eventually produce a candidate containing:
typ relay
For example:
candidate:... udp ... 172.34.151.121 53124 typ relay ...
The important part is:
typ relay
STUN Test
Coturn includes testing utilities in Debian's package. (Debian Packages)
You can test STUN using:
turnutils_stunclient turn.example.com
Or by IP:
turnutils_stunclient 172.34.151.121
TURN Test Utilities
Coturn provides:
turnutils_uclient
turnutils_peer
turnutils_stunclient
turnutils_natdiscovery
Debian 13 installs these with the Coturn package. (Debian Packages)
turnutils_uclient is specifically intended for testing TURN connections and allocations. (GitHub)
Recommended Basic Configuration — Direct Public IP
# =============================================
# Coturn
# Public IP directly assigned to server
# =============================================
listening-port=3478
listening-ip=172.34.151.121
relay-ip=172.34.151.121
min-port=49152
max-port=65535
# Authentication
fingerprint
use-auth-secret
static-auth-secret=CHANGE_TO_RANDOM_SECRET
realm=turn.example.com
# Security
no-multicast-peers
# Logging
verbose
Notice:
NO external-ip
because the server directly owns:
172.34.151.121
Recommended Basic Configuration — Behind NAT
# =============================================
# Coturn
# Server behind NAT
# =============================================
listening-port=3478
listening-ip=10.10.10.20
relay-ip=10.10.10.20
external-ip=172.34.151.121/10.10.10.20
min-port=49152
max-port=65535
# Authentication
fingerprint
use-auth-secret
static-auth-secret=CHANGE_TO_RANDOM_SECRET
realm=turn.example.com
# Security
no-multicast-peers
# Logging
verbose
Router:
TCP 3478 → 10.10.10.20:3478
UDP 3478 → 10.10.10.20:3478
UDP 49152-65535 → 10.10.10.20:49152-65535
TLS / TURNS
For encrypted TURN transport you can also configure:
tls-listening-port=5349
cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem
Then clients can use:
turns:turn.example.com:5349
Firewall:
sudo ufw allow 5349/tcp
If DTLS is being used as well:
sudo ufw allow 5349/udp
DNS Configuration
Create a DNS record:
turn.example.com
│
▼
172.34.151.121
For example:
Type: A
Name: turn
Value: 172.34.151.121
If Coturn is behind NAT, DNS still points to:
172.34.151.121
—not—
10.10.10.20
because Internet clients must contact the public side of the NAT.
Cloudflare DNS
If using Cloudflare DNS, keep the TURN DNS record as:
DNS only
rather than an ordinary HTTP reverse-proxied record.
TURN is not normal HTTP traffic and the standard Cloudflare website proxy does not transparently proxy arbitrary TURN UDP/TCP traffic.
Example Final Architecture — Direct Public IP
Browser
│
│ TURN :3478
│
▼
Internet
│
▼
┌──────────────────────────┐
│ Coturn │
│ │
│ 172.34.151.121 │
│ │
│ UDP/TCP 3478 │
│ UDP 49152-65535 │
└──────────────────────────┘
Configuration:
listening-ip=172.34.151.121
relay-ip=172.34.151.121
Example Final Architecture — Behind NAT
Browser
│
│ TURN
▼
Internet
│
▼
172.34.151.121
┌───────────────────────┐
│ Router / Firewall │
│ │
│ NAT / Port Forwarding │
└───────────┬───────────┘
│
│
▼
10.10.10.20
┌───────────────────────┐
│ Coturn │
│ │
│ Debian 13 │
└───────────────────────┘
Configuration:
listening-ip=10.10.10.20
relay-ip=10.10.10.20
external-ip=172.34.151.121/10.10.10.20
Router:
3478 TCP/UDP
│
▼
10.10.10.20
49152-65535 UDP
│
▼
10.10.10.20
Troubleshooting
No Relay Candidates
If WebRTC produces:
host
srflx
but never:
relay
check:
sudo journalctl -u coturn -f
Then verify:
3478 UDP reachable
3478 TCP reachable
relay UDP range open
username/credential correct
realm correct
external-ip correct
NAT port forwarding correct
TURN Allocation Timeout
Browser errors such as:
TURN allocate request timed out
usually indicate a network path problem.
Check:
Firewall
NAT forwarding
UDP 3478
TCP 3478
Public IP
DNS
Coturn listening IP
Relay Candidate Exists but ICE Never Connects
If you successfully receive:
typ relay
but ICE remains:
checking
the initial TURN control connection may be working while the relay port range is blocked.
Check:
UDP 49152-65535
or whatever you configured as:
min-port=
max-port=
This is particularly important for Coturn behind NAT.
Wrong external-ip
For a server behind NAT:
Public: 172.34.151.121
Private: 10.10.10.20
use:
external-ip=172.34.151.121/10.10.10.20
Coturn's official example configuration describes this public/private mapping syntax for NAT deployments. (GitHub)
Quick Install — Direct Public IP
sudo apt update
sudo apt install -y coturn
sudo systemctl enable coturn
Configure:
sudo nano /etc/turnserver.conf
listening-port=3478
listening-ip=172.34.151.121
relay-ip=172.34.151.121
min-port=49152
max-port=65535
fingerprint
use-auth-secret
static-auth-secret=YOUR_RANDOM_SECRET
realm=turn.example.com
no-multicast-peers
Then:
sudo systemctl restart coturn
sudo systemctl status coturn
Quick Install — Behind NAT
sudo apt update
sudo apt install -y coturn
sudo systemctl enable coturn
Configure:
sudo nano /etc/turnserver.conf
listening-port=3478
listening-ip=10.10.10.20
relay-ip=10.10.10.20
external-ip=172.34.151.121/10.10.10.20
min-port=49152
max-port=65535
fingerprint
use-auth-secret
static-auth-secret=YOUR_RANDOM_SECRET
realm=turn.example.com
no-multicast-peers
Router/NAT:
3478 TCP → 10.10.10.20:3478
3478 UDP → 10.10.10.20:3478
49152-65535 UDP → 10.10.10.20:49152-65535
Then:
sudo systemctl restart coturn
sudo systemctl status coturn
Summary
Coturn with Public IP Directly on Server
listening-ip=PUBLIC_IP
relay-ip=PUBLIC_IP
No NAT mapping is required:
external-ip is normally unnecessary
Coturn Behind NAT
listening-ip=PRIVATE_IP
relay-ip=PRIVATE_IP
external-ip=PUBLIC_IP/PRIVATE_IP
Example:
listening-ip=10.10.10.20
relay-ip=10.10.10.20
external-ip=172.34.151.121/10.10.10.20
Port forwarding is required:
3478 TCP
3478 UDP
49152-65535 UDP
No Publicly Reachable IP at All
If the connection is behind CGNAT and you have:
No public IP
No public NAT forwarding
No publicly reachable endpoint
then a self-hosted Internet-facing TURN server at that location is not practical.
Use:
Public VPS
Cloud VM
Dedicated public server
ISP public/static IP
Managed TURN service
instead.
References
Coturn's official project describes Coturn as a TURN/STUN server for NAT traversal and media relaying. (GitHub)
The official example turnserver.conf documents external-ip and the PUBLIC_IP/PRIVATE_IP mapping required when the TURN server is behind NAT, including the requirement for corresponding relay ports to be directly mapped through NAT. (GitHub)
Debian 13's Coturn package provides /etc/turnserver.conf, /etc/default/coturn, coturn.service, turnserver, turnadmin, and TURN diagnostic utilities. (Debian Packages)





