Media stack¶
Status: Current | Last reviewed: 2026-06-03
The five containers (or pods) that move audio around. None of them have managed-cloud equivalents — operators run them, end of story.
What runs¶
| Service | Role | Default ports | Where it runs |
|---|---|---|---|
| asterisk | PBX, call control via PJSIP + ARI | 5060 (SIP), 8087 (ARI WebSocket), 8088 (HTTP) | Container (Vanguard) / hostNetwork pod (cluster) |
| kamailio | SIP proxy, TLS termination, dial-plan | 5060 (UDP/TCP), 5061 (TLS) | Same |
| janus | WebRTC gateway with SIP plugin | 8088 (HTTP), 8188 (WS), 7088 (admin), 20000-20499 (RTP) | Same |
| rtpengine | RTP/SRTP media proxy, codec transcode | 22222 (control), 20000-20499 (RTP) | Same |
| coturn | STUN/TURN for NAT traversal | 3478 (UDP), 5349 (TLS) | Same |
Why hostNetwork in cluster¶
Media services need:
- Predictable IPs for SIP REGISTER, RTP packet rewriting, and ICE candidate generation. Pod IPs change.
- Raw socket access for fast packet handling.
- Wide port ranges that Kubernetes Services cannot expose efficiently.
The answer is hostNetwork: true. The pod gets the node's IP and port space directly. The trade-off is that pods are pinned to nodes one-to-one — no Service abstraction.
Charts that use this pattern: charts/asterisk/templates/deployment.yaml, charts/kamailio/, charts/janus/, charts/rtpengine/, charts/coturn/.
Per-cloud media node pools¶
The media pods need dedicated nodes (kernel tuning, dedicated NICs, no preemption). Each cloud's values file pins them:
- GCP:
nodeSelector: cloud.google.com/gke-nodepool: media-pool. Operators provision the pool with kernel tuning andsysctloverrides. - AWS: tolerations + labels matching dedicated EC2 instances. See media-pool-tuning.md.
- Azure: same tolerations approach with AKS node pools.
kernel module unavailable in cluster¶
rtpengine performs best with its kernel module loaded — packet forwarding happens in-kernel without a userspace round-trip. In GKE, EKS, and AKS, container nodes do not allow loading kernel modules. The chart enforces userspace forwarding with table = -1 (see charts/rtpengine/templates/configmap.yaml).
Userspace works fine for hundreds of concurrent calls per node. If a customer needs thousands per node, they need bare-metal or self-managed VMs where the module can load.
Global Accelerator and dual-interface RTP¶
When AWS Global Accelerator fronts the SIP/media plane, it DNATs the public anycast IP to the regional LAN IP before delivery. Single-interface rtpengine config (interface = ${PUBLIC_IP}) breaks because rtpengine's listening socket binds the public IP, but inbound packets arrive on the LAN IP — kernel returns ICMP port-unreachable.
Fix: dual-interface in docker-compose.media.yml and the chart values:
interface = lan/${LAN_IP}!${PUBLIC_IP}
interface = pub/${PUBLIC_IP}
Sentinel and any GA-fronted environment must use this form. Direct IP environments can keep the single-interface config.
kamailio per-city TLS¶
Each city dev environment ships with a dedicated kamailio TLS cert. The cert maps to a Genesys BYOC trunk; the trunk has an ACL allowlisting the city's NAT egress IP. Three-way constraint: GA TCP :5065, per-city cert, trunk ACL — all three must align or Genesys transfers 404.
Janus WebSocket header rules¶
The nginx ingress for Janus's /ws endpoint must strip Cookie, User-Agent, and Referer, and re-declare Upgrade, Connection, and Host. nginx's header inheritance rules mean partial declarations get clobbered. See configs/nginx/trueserve-vanguard.conf.
Symptom of getting it wrong: Chrome 502 on SIP registration.
Where each one fits in the call path¶
Caller / Carrier (SIP)
→ kamailio (TLS, ACL, dial-plan, inter-city routing)
→ asterisk (PJSIP B2BUA, dialplan, ARI handoff)
→ ari-controller (Stasis app; orchestrates the call)
→ media-proxy (carries audio between Asterisk and Pipeline)
Web caller (WebRTC)
→ coturn (STUN/TURN for NAT traversal)
→ janus (WebRTC ↔ SIP bridge)
→ asterisk (rest of the path same as above)
For SRTP:
rtpengine sits between asterisk and the outside world,
rewriting SDP and forwarding packets.