|
| 1 | +# Self-Hosted LiveKit on AWS |
| 2 | + |
| 3 | +Last updated: 2026-05-25 |
| 4 | + |
| 5 | +This document describes a practical AWS deployment shape for the official |
| 6 | +LiveKit Server using EC2, ECS on EC2, or EKS, with Network Load Balancers, |
| 7 | +ElastiCache Redis, security groups, and TURN. It is an architecture and |
| 8 | +operations guide, not a Terraform module. |
| 9 | + |
| 10 | +## Goals |
| 11 | + |
| 12 | +- Run the official LiveKit media server, not a custom SFU implementation. |
| 13 | +- Keep signaling behind TLS while exposing the WebRTC media ports that LiveKit |
| 14 | + advertises to clients. |
| 15 | +- Support single-node development, multi-node production, and Kubernetes/ECS |
| 16 | + variants from the same network model. |
| 17 | +- Use Redis for redundant multi-node deployments. |
| 18 | +- Keep security groups explicit enough that failed calls can be debugged from |
| 19 | + the port table. |
| 20 | + |
| 21 | +## Core Assumptions |
| 22 | + |
| 23 | +- Public clients connect to `wss://livekit.example.com`. |
| 24 | +- TURN uses a separate name such as `turn.example.com`. |
| 25 | +- API keys are generated and stored outside the repository. |
| 26 | +- LiveKit Server runs from the official binary or Docker image. |
| 27 | +- Production nodes have public routability for advertised ICE candidates, or |
| 28 | + clients are forced through TURN. |
| 29 | + |
| 30 | +## First Decision |
| 31 | + |
| 32 | +For production, prefer this order: |
| 33 | + |
| 34 | +1. EKS with the official LiveKit Helm chart when the team already operates |
| 35 | + Kubernetes. |
| 36 | +2. ECS on EC2 when the team wants container orchestration without Kubernetes. |
| 37 | +3. Plain EC2 with Docker Compose/systemd for the smallest reliable footprint. |
| 38 | + |
| 39 | +Avoid Fargate or private/serverless-only clusters for the media server. LiveKit |
| 40 | +needs direct network access for RTC ports, and the official Kubernetes guidance |
| 41 | +requires host networking with one LiveKit pod per node. |
| 42 | + |
| 43 | +## Network Model |
| 44 | + |
| 45 | +LiveKit is not only an HTTPS application. The API and WebSocket endpoint can sit |
| 46 | +behind a TLS-terminating load balancer, but RTC traffic must reach the LiveKit |
| 47 | +node and port that the server advertises in ICE candidates. |
| 48 | + |
| 49 | +Recommended split: |
| 50 | + |
| 51 | +- `livekit.example.com`: TLS endpoint for API and WebSocket signaling. |
| 52 | +- `turn.example.com`: TURN/TLS and TURN/UDP endpoint. |
| 53 | +- LiveKit nodes: directly reachable for ICE/UDP and ICE/TCP, unless the |
| 54 | + deployment intentionally uses TURN-only connectivity. |
| 55 | +- Redis: private only. |
| 56 | + |
| 57 | +Redis enables distributed LiveKit routing. When Redis is configured, LiveKit |
| 58 | +uses it for room data and internal messaging so a client can connect to any |
| 59 | +node and still be bridged to the node hosting the room. A room still needs to |
| 60 | +fit on one LiveKit node. |
| 61 | + |
| 62 | +## Port Matrix |
| 63 | + |
| 64 | +| Purpose | Default | Direction | Public? | Notes | |
| 65 | +| --- | ---: | --- | --- | --- | |
| 66 | +| API and WebSocket | `7880/TCP` | Client to LiveKit through LB | No direct node exposure | Put behind TLS on `443`. | |
| 67 | +| ICE/UDP | `50000-60000/UDP` | Client to LiveKit node | Yes | Default RTC media range. Each participant can use multiple ports. | |
| 68 | +| ICE/TCP fallback | `7881/TCP` | Client to LiveKit node | Yes | LiveKit notes this cannot sit behind TLS/LB. | |
| 69 | +| ICE/UDP mux | `7882/UDP` or range | Client to LiveKit node | Yes | Optional alternative to the wide UDP range. Remove `port_range_start/end` when using it. | |
| 70 | +| TURN/TLS | `5349/TCP` | Client to TURN | Usually through NLB | Use `443` for best restrictive-firewall coverage if using a dedicated TURN LB/IP. | |
| 71 | +| TURN/UDP | `3478/UDP` | Client to TURN | Yes | Can also be `443/UDP` if not used by another UDP service. | |
| 72 | +| Redis | `6379/TCP` | LiveKit to Redis | No | Private subnets only. Prefer TLS and AUTH/RBAC. | |
| 73 | +| Prometheus | `6789/TCP` | Monitoring to LiveKit | No | Only enable from monitoring subnets/security groups. | |
| 74 | +| SSH | `22/TCP` | Admin to node | No public if possible | Prefer SSM Session Manager. | |
| 75 | + |
| 76 | +Optional SIP/Ingress/Egress ports should be added only when those components |
| 77 | +are deployed. |
| 78 | + |
| 79 | +## Load Balancers |
| 80 | + |
| 81 | +Use an internet-facing Network Load Balancer for layer-4 traffic that must stay |
| 82 | +close to the transport layer. AWS NLB target groups support TCP, TLS, UDP, |
| 83 | +TCP_UDP, QUIC, and TCP_QUIC protocols. |
| 84 | + |
| 85 | +Recommended listeners: |
| 86 | + |
| 87 | +| DNS name | Listener | Target | Health check | |
| 88 | +| --- | --- | --- | --- | |
| 89 | +| `livekit.example.com` | `443/TLS` | LiveKit `7880/TCP` | `TCP:7880` | |
| 90 | +| `turn.example.com` | `443/TLS` or `5349/TLS` | LiveKit TURN TLS port | `TCP:7880` or a dedicated TCP check | |
| 91 | +| `turn.example.com` | `3478/UDP` or `443/UDP` | LiveKit TURN UDP port | Non-UDP check, usually `TCP:7880` | |
| 92 | + |
| 93 | +If both API and TURN/TLS must use public port `443`, use separate NLBs or |
| 94 | +separate public IPs. NLB TLS can select certificates with SNI, but it is not an |
| 95 | +HTTP router and should not be treated like ALB host-based routing. |
| 96 | + |
| 97 | +Do not plan to publish the default `50000-60000/UDP` media range through a |
| 98 | +single generic listener. Either expose the node media ports directly with |
| 99 | +security groups, use an explicitly tested UDP mux design, or rely on TURN for |
| 100 | +restricted networks. |
| 101 | + |
| 102 | +For UDP target groups, use a TCP or HTTP health check. AWS documents that UDP |
| 103 | +and QUIC services are checked with non-UDP health checks. |
| 104 | + |
| 105 | +## Security Groups |
| 106 | + |
| 107 | +Create separate security groups for the NLB, LiveKit nodes/tasks, Redis, and |
| 108 | +monitoring. |
| 109 | + |
| 110 | +### NLB Security Group |
| 111 | + |
| 112 | +Inbound: |
| 113 | + |
| 114 | +- `443/TCP` from `0.0.0.0/0` and `::/0` for API TLS. |
| 115 | +- `443/TCP` or `5349/TCP` from `0.0.0.0/0` and `::/0` for TURN/TLS. |
| 116 | +- `3478/UDP` or `443/UDP` from `0.0.0.0/0` and `::/0` for TURN/UDP. |
| 117 | + |
| 118 | +Outbound: |
| 119 | + |
| 120 | +- To the LiveKit node security group on `7880/TCP`. |
| 121 | +- To the LiveKit node security group on the chosen TURN/TCP and TURN/UDP ports. |
| 122 | + |
| 123 | +### LiveKit Node Security Group |
| 124 | + |
| 125 | +Inbound: |
| 126 | + |
| 127 | +- `7880/TCP` from the NLB security group. |
| 128 | +- `7881/TCP` from `0.0.0.0/0` and `::/0` if ICE/TCP fallback is enabled. |
| 129 | +- `50000-60000/UDP` from `0.0.0.0/0` and `::/0` if using the default media range. |
| 130 | +- The configured `rtc.udp_port` UDP port or range from `0.0.0.0/0` and `::/0` |
| 131 | + if using UDP mux instead of the default media range. |
| 132 | +- TURN/TCP and TURN/UDP ports from the NLB security group, or from the internet |
| 133 | + if TURN is exposed directly. |
| 134 | +- `6789/TCP` from the monitoring security group if Prometheus is enabled. |
| 135 | +- `22/TCP` only from a trusted admin CIDR, or omit it and use SSM. |
| 136 | + |
| 137 | +Outbound: |
| 138 | + |
| 139 | +- `6379/TCP` to the Redis security group. |
| 140 | +- `443/TCP` to the internet for image pulls, certificates, package updates, and |
| 141 | + optional STUN discovery. |
| 142 | +- UDP/TCP ephemeral outbound as required by the operating system and LiveKit |
| 143 | + media flows. |
| 144 | + |
| 145 | +### Redis Security Group |
| 146 | + |
| 147 | +Inbound: |
| 148 | + |
| 149 | +- `6379/TCP` only from LiveKit node/task/pod security groups. |
| 150 | + |
| 151 | +Outbound: |
| 152 | + |
| 153 | +- Default VPC egress is normally enough. |
| 154 | + |
| 155 | +## Redis |
| 156 | + |
| 157 | +Use Amazon ElastiCache for Valkey or Redis OSS. |
| 158 | + |
| 159 | +Starter production shape: |
| 160 | + |
| 161 | +- Replication group, cluster mode disabled. |
| 162 | +- Multi-AZ with automatic failover. |
| 163 | +- One primary and at least one replica in a different Availability Zone. |
| 164 | +- In-transit encryption enabled. |
| 165 | +- AUTH token or RBAC enabled. |
| 166 | +- Private subnets only. |
| 167 | + |
| 168 | +LiveKit does not need Redis to be public. Store the endpoint and credentials in |
| 169 | +AWS Secrets Manager, SSM Parameter Store, or the platform-native secret system. |
| 170 | + |
| 171 | +## LiveKit Configuration |
| 172 | + |
| 173 | +Baseline config: |
| 174 | + |
| 175 | +```yaml |
| 176 | +port: 7880 |
| 177 | +log_level: info |
| 178 | + |
| 179 | +rtc: |
| 180 | + tcp_port: 7881 |
| 181 | + port_range_start: 50000 |
| 182 | + port_range_end: 60000 |
| 183 | + use_external_ip: true |
| 184 | + |
| 185 | +redis: |
| 186 | + address: <elasticache-primary-endpoint>:6379 |
| 187 | + username: default |
| 188 | + password: <redis-password> |
| 189 | + tls: |
| 190 | + enabled: true |
| 191 | + insecure: false |
| 192 | + server_name: <elasticache-primary-endpoint> |
| 193 | + |
| 194 | +keys: |
| 195 | + <api-key>: <api-secret> |
| 196 | + |
| 197 | +turn: |
| 198 | + enabled: true |
| 199 | + domain: turn.example.com |
| 200 | + udp_port: 3478 |
| 201 | + tls_port: 5349 |
| 202 | + external_tls: true |
| 203 | + relay_range_start: 1024 |
| 204 | + relay_range_end: 30000 |
| 205 | + |
| 206 | +prometheus_port: 6789 |
| 207 | +``` |
| 208 | +
|
| 209 | +Only set `turn.external_tls: true` when a layer-4 load balancer terminates TLS |
| 210 | +before forwarding traffic to LiveKit. If LiveKit terminates TURN/TLS itself, |
| 211 | +provide `cert_file` and `key_file` instead. |
| 212 | + |
| 213 | +UDP mux alternative: |
| 214 | + |
| 215 | +```yaml |
| 216 | +rtc: |
| 217 | + tcp_port: 7881 |
| 218 | + udp_port: 7882-7890 |
| 219 | + use_external_ip: true |
| 220 | +``` |
| 221 | + |
| 222 | +When using `rtc.udp_port`, remove `port_range_start` and `port_range_end`. |
| 223 | +LiveKit recommends a UDP mux range at least as large as the node vCPU count for |
| 224 | +better performance. |
| 225 | + |
| 226 | +## EC2 Deployment |
| 227 | + |
| 228 | +Use this for the simplest production-capable setup. |
| 229 | + |
| 230 | +1. Create public subnets in at least two Availability Zones. |
| 231 | +2. Create the NLB listeners and target groups from this document. |
| 232 | +3. Create an ElastiCache replication group in private subnets. |
| 233 | +4. Launch compute-optimized EC2 instances with enhanced networking. |
| 234 | +5. Run LiveKit with Docker host networking or the native binary. |
| 235 | +6. Register instances in the NLB target groups. |
| 236 | +7. Configure Auto Scaling lifecycle hooks or deployment automation to send |
| 237 | + `SIGTERM` and wait for LiveKit draining before terminating instances. |
| 238 | + |
| 239 | +Docker Compose shape: |
| 240 | + |
| 241 | +```yaml |
| 242 | +services: |
| 243 | + livekit: |
| 244 | + image: livekit/livekit-server:<pinned-version> |
| 245 | + network_mode: host |
| 246 | + restart: unless-stopped |
| 247 | + command: --config /etc/livekit/livekit.yaml |
| 248 | + volumes: |
| 249 | + - /etc/livekit/livekit.yaml:/etc/livekit/livekit.yaml:ro |
| 250 | +``` |
| 251 | + |
| 252 | +Pin the LiveKit image version that was tested in staging. Avoid `latest` in |
| 253 | +production. |
| 254 | + |
| 255 | +## ECS on EC2 Deployment |
| 256 | + |
| 257 | +Use ECS only on EC2 for the media server. |
| 258 | + |
| 259 | +Recommended ECS settings: |
| 260 | + |
| 261 | +- Launch type or capacity provider: EC2. |
| 262 | +- Task network mode: `host`. |
| 263 | +- Placement: one LiveKit task per EC2 instance. |
| 264 | +- Placement constraint: `distinctInstance` or a capacity model that enforces |
| 265 | + one task per host. |
| 266 | +- Target groups: register the ECS service with the API/TURN target groups. |
| 267 | +- Secrets: inject LiveKit config or sensitive values from Secrets Manager/SSM. |
| 268 | + |
| 269 | +Host networking is supported for ECS tasks on EC2, but not on Fargate. AWS also |
| 270 | +notes that host mode prevents running multiple copies of the same task on one |
| 271 | +host when the same ports are required. That limitation matches LiveKit's |
| 272 | +networking model. |
| 273 | + |
| 274 | +Use `awsvpc` only after explicit validation. ECS supports port ranges, but |
| 275 | +LiveKit's Docker guidance prefers host networking for optimal media behavior. |
| 276 | + |
| 277 | +## EKS Deployment |
| 278 | + |
| 279 | +Use the official LiveKit Helm chart when deploying to EKS. |
| 280 | + |
| 281 | +EKS requirements: |
| 282 | + |
| 283 | +- AWS Load Balancer Controller installed if using AWS-managed ingress/load |
| 284 | + balancers from Kubernetes resources. |
| 285 | +- LiveKit pods with host networking. |
| 286 | +- One LiveKit pod per node. |
| 287 | +- Dedicated or labeled node group for LiveKit nodes. |
| 288 | +- Publicly routable nodes for direct ICE, or TURN-only connectivity. |
| 289 | +- External ElastiCache Redis, not an in-cluster single pod Redis for production. |
| 290 | +- Long termination grace period so LiveKit can drain active rooms. |
| 291 | + |
| 292 | +Operational pattern: |
| 293 | + |
| 294 | +1. Create or reuse an EKS cluster with public ingress for LiveKit. |
| 295 | +2. Create a LiveKit node group sized for network bandwidth and CPU. |
| 296 | +3. Install Redis externally with Multi-AZ. |
| 297 | +4. Install the LiveKit Helm chart with Redis, TURN, and RTC settings. |
| 298 | +5. Verify that pod scheduling keeps one LiveKit pod per node. |
| 299 | +6. Verify NLB/Ingress resources and DNS records. |
| 300 | +7. Run connect, TURN-only, weak-network, and multi-participant tests before |
| 301 | + production traffic. |
| 302 | + |
| 303 | +## DNS and Certificates |
| 304 | + |
| 305 | +Recommended records: |
| 306 | + |
| 307 | +- `livekit.example.com` -> API NLB alias record. |
| 308 | +- `turn.example.com` -> TURN NLB alias record. |
| 309 | + |
| 310 | +Use ACM certificates for NLB TLS termination. If LiveKit terminates TURN/TLS |
| 311 | +itself, provision the TURN certificate onto the node or into the Kubernetes |
| 312 | +secret expected by the Helm chart. |
| 313 | + |
| 314 | +## Verification Checklist |
| 315 | + |
| 316 | +Before production: |
| 317 | + |
| 318 | +- `livekit.example.com` resolves to the API NLB. |
| 319 | +- `turn.example.com` resolves to the TURN NLB or direct TURN endpoint. |
| 320 | +- NLB target groups are healthy. |
| 321 | +- LiveKit can connect to Redis over TLS. |
| 322 | +- A generated token can connect to `wss://livekit.example.com`. |
| 323 | +- A browser/mobile client can join, publish, subscribe, leave, and reconnect. |
| 324 | +- UDP media succeeds without TURN from a normal network. |
| 325 | +- TURN/UDP succeeds from a restricted test network. |
| 326 | +- TURN/TLS succeeds from a network that blocks UDP. |
| 327 | +- ICE/TCP fallback is verified if `rtc.tcp_port` is enabled. |
| 328 | +- Redis failover is tested in staging. |
| 329 | +- Node termination sends `SIGTERM` and LiveKit drains instead of dropping rooms. |
| 330 | +- Prometheus metrics are scraped from a private monitoring path only. |
| 331 | + |
| 332 | +Useful client tests: |
| 333 | + |
| 334 | +- Normal Wi-Fi/cellular connection. |
| 335 | +- VPN or corporate network. |
| 336 | +- UDP-blocked network. |
| 337 | +- TURN-only forced test. |
| 338 | +- Multi-participant room. |
| 339 | +- Long-running room soak. |
| 340 | + |
| 341 | +## Common Failure Modes |
| 342 | + |
| 343 | +- Clients connect to WebSocket but media never flows: RTC UDP/TCP ports are not |
| 344 | + reachable, or LiveKit advertised a private IP. |
| 345 | +- TURN/TLS fails behind corporate networks: the public TURN port is not `443`, |
| 346 | + the certificate domain does not match, or TLS is terminated in the wrong |
| 347 | + place. |
| 348 | +- Multi-node rooms behave inconsistently: Redis is missing, unreachable, or |
| 349 | + configured without the same credentials on all LiveKit nodes. |
| 350 | +- NLB UDP target is unhealthy: health checks are pointed at UDP instead of a |
| 351 | + TCP/HTTP health path. |
| 352 | +- Rolling deploys drop calls: compute is terminated without allowing LiveKit's |
| 353 | + native draining behavior to complete. |
| 354 | +- EKS schedules multiple LiveKit pods on one node: host networking and port |
| 355 | + ownership are not being respected in scheduling constraints. |
| 356 | + |
| 357 | +## References |
| 358 | + |
| 359 | +- [LiveKit self-hosting overview](https://docs.livekit.io/transport/self-hosting/) |
| 360 | +- [LiveKit deployment guide](https://docs.livekit.io/transport/self-hosting/deployment/) |
| 361 | +- [LiveKit ports and firewall](https://docs.livekit.io/transport/self-hosting/ports-firewall/) |
| 362 | +- [LiveKit virtual machines guide](https://docs.livekit.io/transport/self-hosting/vm/) |
| 363 | +- [LiveKit Kubernetes guide](https://docs.livekit.io/transport/self-hosting/kubernetes/) |
| 364 | +- [LiveKit distributed multi-region guide](https://docs.livekit.io/transport/self-hosting/distributed/) |
| 365 | +- [LiveKit config sample](https://github.com/livekit/livekit/blob/master/config-sample.yaml) |
| 366 | +- [AWS Network Load Balancer overview](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/introduction.html) |
| 367 | +- [AWS NLB listeners](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-listeners.html) |
| 368 | +- [AWS NLB target group health checks](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/target-group-health-checks.html) |
| 369 | +- [Amazon ECS task networking on EC2](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html) |
| 370 | +- [Amazon ECS host network mode](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/networking-networkmode-host.html) |
| 371 | +- [ElastiCache in-transit encryption](https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/in-transit-encryption.html) |
| 372 | +- [ElastiCache Multi-AZ automatic failover](https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/AutoFailover.html) |
0 commit comments