Over 10 years we help companies reach their financial and branding goals. Engitech is a values-driven technology agency dedicated.

Gallery

Contacts

411 University St, Seattle, USA

+1 -800-456-478-23

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)

Leave a comment

Your email address will not be published. Required fields are marked *