Migration Guide: From Coturn to Embedded STUN Server
This guide helps you migrate your existing NetBird self-hosted installation from using an external Coturn TURN server to the new embedded STUN server integrated into the relay service.
Who is this guide for? This migration guide is intended for users who:
- Have an existing NetBird self-hosted deployment with Coturn configured for STUN (NAT traversal)
- Already have the NetBird Relay service running for relayed connections
If you were using Coturn purely for TURN relay functionality, note that NetBird Relay has replaced TURN for relayed connections. This guide focuses on migrating the STUN functionality from Coturn to the embedded STUN server in the relay service.
Overview of Changes
What's New
- Embedded STUN Server: The relay service now includes a built-in STUN server, eliminating the need for a separate Coturn container
- Simplified Architecture: Fewer containers to manage (no more Coturn)
- Easier Configuration: Simple environment variables instead of complex
turnserver.conf - Better Integration: STUN server is tightly integrated with the NetBird relay service
What's Removed
- Coturn container: No longer needed
- turnserver.conf: Configuration file no longer required
Prerequisites
- NetBird relay image updated to the latest version
- Backup of your current configuration files
- Brief maintenance window (clients will reconnect automatically)
Migration Steps
Step 1: Backup Current Configuration
# Create a backup directory
mkdir -p netbird-backup-$(date +%Y%m%d)
cd netbird-backup-$(date +%Y%m%d)
# Backup all configuration files
cp ../docker-compose.yml .
cp ../management.json .
cp ../turnserver.conf . 2>/dev/null || echo "No turnserver.conf found"
cp ../*.env . 2>/dev/null || echo "No .env files found"
Step 2: Update docker-compose.yml
Remove the Coturn Service
Delete the entire coturn service block from your docker-compose.yml:
# REMOVE THIS ENTIRE BLOCK:
coturn:
image: coturn/coturn:latest
restart: unless-stopped
volumes:
- ./turnserver.conf:/etc/turnserver.conf:ro
network_mode: host
command:
- -c /etc/turnserver.conf
Update the Relay Service
Add STUN configuration to your relay service. You need to expose the STUN UDP port(s) in the ports section. You can configure multiple STUN ports if needed (e.g., for different network interfaces or load balancing) - ensure each port is exposed in docker-compose.yml and listed in NB_STUN_PORTS:
relay:
image: netbirdio/relay:latest
container_name: netbird-relay
restart: unless-stopped
networks: [netbird]
ports:
- '3478:3478/udp' # STUN UDP port (add more lines if using multiple ports)
# - '3479:3479/udp' # Example: additional STUN port (optional)
env_file:
- ./relay.env
# OR use environment directly:
# environment:
# - NB_LOG_LEVEL=info
# - NB_LISTEN_ADDRESS=:80
# - NB_EXPOSED_ADDRESS=rels://your-domain.com:443
# - NB_AUTH_SECRET=your-relay-secret
# - NB_ENABLE_STUN=true # Enable embedded STUN
# - NB_STUN_LOG_LEVEL=info # STUN logging level
# - NB_STUN_PORTS=3478 # STUN port(s), comma-separated for multiple (e.g., 3478,3479)
logging:
driver: "json-file"
options:
max-size: "500m"
max-file: "2"
Step 3: Create or Update relay.env
Create a relay.env file with the STUN configuration:
# relay.env
NB_LOG_LEVEL=info
NB_LISTEN_ADDRESS=:80
NB_EXPOSED_ADDRESS=rels://your-domain.com:443
NB_AUTH_SECRET=your-existing-relay-secret
# New STUN configuration
NB_ENABLE_STUN=true
NB_STUN_LOG_LEVEL=info
NB_STUN_PORTS=3478
# For multiple ports, use comma-separated values:
# NB_STUN_PORTS=3478,3479
Keep your existing NB_AUTH_SECRET value from your current configuration. If using multiple STUN ports, ensure all ports are exposed in your docker-compose.yml and listed in NB_STUN_PORTS.
Step 4: Update management.json
Update the Stuns section in your management.json to point to the embedded STUN server:
Before (with Coturn):
{
"Stuns": [
{
"Proto": "udp",
"URI": "stun:your-coturn-domain:3478" // coturn domain and port
}
],
"Relay": {
"Addresses": ["rels://your-relay-domain.com:443"], // NetBird Relay domain and port
"CredentialsTTL": "24h",
"Secret": "your-relay-secret"
},
...
}
After (with embedded STUN):
{
"Stuns": [
{
"Proto": "udp",
"URI": "stun:your-relay-domain:3478" // NetBird Relay domain and port
}
],
"Relay": {
"Addresses": ["rels://your-relay-domain.com:443"], // NetBird Relay domain and port
"CredentialsTTL": "24h",
"Secret": "your-relay-secret"
},
...
}
Remove the TURNConfig section entirely as TURN is replaced by the NetBird relay service.
Step 5: Update Firewall Rules
Ensure your firewall allows:
| Port | Protocol | Purpose |
|---|---|---|
| 3478 | UDP | STUN server (NAT traversal) |
| 443 | TCP | Relay, Management API, Signal (via reverse proxy) |
If you previously had TURN-specific firewall rules (e.g., UDP port ranges 49152-65535 for TURN relay), these are no longer needed.
Step 6: Apply Changes
# Stop all services
docker compose down
# Remove the old turnserver.conf (optional, for cleanup)
rm -f turnserver.conf
# Pull latest images
docker compose pull
# Start services
docker compose up -d
# Verify services are running
docker compose ps
# Check relay logs for STUN initialization
docker compose logs relay | grep -i stun
You should see log messages indicating the STUN server has started:
level=info msg="Starting STUN server on port 3478"
Step 7: Verify Migration
-
Check STUN Server:
Enable debug logging for the STUN server by setting
NB_STUN_LOG_LEVEL=debugin yourrelay.env, then restart the relay service and observe the logs:docker logs --tail 100 -f netbird-relayWhen a client connects, you should see STUN binding requests being processed:
2026-01-21T12:08:54Z DEBG [component: stun-server] stun/server.go:107: [port:3478] received 20 bytes from 90.66.38.219:58044 2026-01-21T12:08:54Z DEBG [component: stun-server] stun/server.go:122: [port:3478] received STUN Binding request from 90.66.38.219:58044 (tx=66d2c3e114d74ab1) 2026-01-21T12:08:54Z DEBG [component: stun-server] stun/server.go:152: [port:3478] sent STUN BindingSuccess to 90.66.38.219:58044 (40 bytes) with XORMappedAddress 90.66.38.219:58044 -
Check Client Connectivity:
- Connect a NetBird client
- Verify peer connections are established
- Check the dashboard for connected peers
-
Review Logs:
docker compose logs -f relay docker compose logs -f management
Configuration Reference
Relay Environment Variables
| Variable | Description | Default |
|---|---|---|
NB_ENABLE_STUN | Enable embedded STUN server | false |
NB_STUN_PORTS | STUN UDP port(s), comma-separated | 3478 |
NB_STUN_LOG_LEVEL | STUN server log level | info |
Relay CLI Flags (Alternative)
If running the relay binary directly:
./relay \
--enable-stun \
--stun-ports 3478 \
--stun-log-level info
Troubleshooting
STUN Server Not Starting
Symptom: No STUN-related log messages
Solution: Verify NB_ENABLE_STUN=true is set and the relay container has port 3478/udp exposed.
Clients Can't Connect
Symptom: Clients fail to establish peer connections
Checklist:
- Verify port 3478/udp is open in firewall
- Check
management.jsonhas correct STUN URI - Ensure relay service is running:
docker compose ps - Check relay logs:
docker compose logs relay
Port Already in Use
Symptom: Error: address already in use for port 3478
Solution:
- Check if old Coturn is still running:
docker ps -a | grep coturn - Stop and remove old Coturn:
docker rm -f coturn - Check for other services using port 3478:
netstat -tulpn | grep 3478
NAT Traversal Issues
Symptom: Connections work locally but fail across NAT
Solution: Ensure the STUN URI in management.json uses your public domain/IP that clients can reach.
Fresh Installation
For new installations, use the updated getting-started.sh script which automatically configures the embedded STUN server:
curl -sSL https://github.com/netbirdio/netbird/raw/main/infrastructure_files/getting-started.sh | bash
Rollback Procedure
If you need to rollback to Coturn:
# Stop services
docker compose down
# Restore backup
cd netbird-backup-YYYYMMDD
cp docker-compose.yml ../
cp management.json ../
cp turnserver.conf ../
# Restart with old configuration
cd ..
docker compose up -d

