Coturn and Kurento in Docker Compose: a working network layout

Coturn and Kurento should not share the same networking assumptions. Coturn needs public UDP reachability and a large relay-port range. Kurento needs a stable internal signaling endpoint and a public media address that does not expose Docker's private network.

Separate the three concerns

A working deployment has three different paths:

The first two paths are media paths. You cannot validate them by confirming that HTTPS, signaling or Docker DNS is working.

Keep coturn on host networking

On a typical Linux VPS, network_mode: host is the least surprising starting point for coturn. A bridge network can work, but it adds another NAT layer in front of the relay range and makes candidate debugging harder.

When host networking is used, remove the ports: block. Docker does not publish host-network ports, and leaving those mappings in the file makes it look as if port translation is happening when it is not.

# /etc/coturn/turnserver.conf
listening-port=3478
fingerprint
use-auth-secret
static-auth-secret=replace-with-a-long-random-secret
realm=turn.example.com
external-ip=203.0.113.10
min-port=49152
max-port=65535
verbose
no-cli
services:
  coturn:
    image: coturn/coturn:4.6.3
    network_mode: host
    restart: unless-stopped
    volumes:
      - ./turnserver.conf:/etc/coturn/turnserver.conf:ro
    command: ["-c", "/etc/coturn/turnserver.conf"]

If the VPS has a private interface and the provider maps a public address to it, use the paired form of external-ip:

external-ip=203.0.113.10/10.0.0.4

Replace 10.0.0.4 with the address coturn actually sees on the host. Public first, local second. Without this mapping, the relayed candidate can contain an address that no remote peer can route to.

Keep Kurento on the Compose network

Kurento should not use host networking just because coturn does. Its WebSocket endpoint is a private service consumed by the Spring Boot application, and its media path needs an explicit public address.

services:
  kurento:
    image: kurento/kurento-media-server:7.1.0
    restart: unless-stopped
    expose:
      - "8888"
    environment:
      KMS_STUN_IP: turn.example.com
      KMS_STUN_PORT: "3478"
      KMS_TURN_URL: "turn:turn.example.com:3478?transport=udp"
      KMS_EXTERNAL_IPV4: "203.0.113.10"
    networks:
      - server

  server-backend:
    build: .
    ports:
      - "8090:8080"
    depends_on:
      - kurento
    networks:
      - server

networks:
  server:
    driver: bridge

The distinction between the two addresses matters:

If Kurento advertises a 172.x or 10.x address in its SDP, a remote browser has no route to it. The signaling connection may still look healthy because the browser is talking to the public application origin, not to that private media address.

Do not confuse realm, hostname and relay address

These settings have different jobs:

Use a stable hostname such as turn.example.com for the realm and the client URL. Use the public IP for external-ip when coturn cannot discover it itself. The realm is not the public IP, and changing it is an authentication-context change.

Choose one TURN credential mode

Coturn supports static long-term credentials and time-limited REST credentials. They are alternative mechanisms, not two layers to enable casually.

For a development box, lt-cred-mech with an explicit test user is simple. For a public WebRTC application, use-auth-secret is usually the better fit: the application server generates a short-lived credential for each client.

username = <unix-expiry>:<label>
password = base64(HMAC-SHA1(secret, username))

If you switch to use-auth-secret, remove static user= lines unless a separate internal credential path actually needs them. Do not ship a long-lived shared browser credential inside JavaScript.

Kurento is an internal client of the TURN service. If it needs a fixed URL, give that path its own tightly scoped credential and rotate it deliberately. Do not reuse a browser credential for an internal service.

Open the relay range in both firewalls

Port 3478 is the control port. RTP does not flow over it. Coturn allocates a relay port for each session from the configured UDP range, normally 49152-65535.

Open that range in both the provider security group and the host firewall. A successful TURN allocation only proves that the control exchange worked; it does not prove that the relay can carry media.

A ten-port range is a lab configuration. Concurrent allocations can consume more than one port per calling user, and retries make the demand less predictable. Keep the default range or size a narrower range from measured concurrency.

Keep UDP first and add fallbacks deliberately

Start with:

turn:turn.example.com:3478?transport=udp

Add TCP on 3478 for networks that block UDP. If you terminate TLS for TURN, add turns: on 5349 or 443. Do not put TURN behind an HTTP reverse proxy: TURN is not HTTP, and a generic proxy configuration will not relay it.

Give Nginx only the application boundary

Nginx can terminate public HTTPS and WSS for the application. It can also proxy the Kurento WebSocket over the private Compose network when that endpoint must be exposed to the browser through the product.

Keep port 8888 unexposed to the public internet. Keep coturn's control and relay ports outside the HTTP proxy. The reverse proxy belongs on the application path, not on the media relay path.

Verify the deployment in this order

  1. Confirm coturn is listening on UDP 3478 and the configured relay range.
  2. Confirm both the provider firewall and the host firewall allow the same relay UDP range.
  3. Use the Trickle ICE page and confirm that a relay candidate appears.
  4. Set iceTransportPolicy: "relay" in a test browser and confirm that a call still connects.
  5. Inspect Kurento SDP and confirm that it advertises the public media address, not a Docker address.
  6. Test from cellular data or another network, not from the VPS's own LAN.

The common failure is not "Docker is broken." It is a public service configured with an internal address, a relay range that is open in only one firewall layer, or a browser credential mode that does not match the server's authentication mode.

The Low-Latency Kit source package includes a Go signaling server and SFU, browser clients, reconnect handling, diagnostics and deployment notes for a self-hosted real-time video path.

Previous: Pion ReplaceTrack -> Next: coturn authentication ->