Deployment guide

LiveKit

Videos

Overview: LiveKit 1.13 on Azure: overview (watch on YouTube)

Launch and log in: LiveKit 1.13 on Azure: launch and log in (watch on YouTube)

Deep Dive: LiveKit 1.13 on Azure: deep dive (watch on YouTube)

Overview

LiveKit is the open-source WebRTC infrastructure server, a Selective Forwarding Unit (SFU) that powers real-time audio, video and data for live streaming, video conferencing and real-time AI voice and agent applications. Clients join rooms using signed JWT access tokens that your application mints server-side from an API key and secret.

This guide covers the deployment and configuration of LiveKit on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images. The image installs the current stable LiveKit 1.13 server, the official prebuilt livekit-server Go binary, and runs it directly under systemd. Docker is not required. The LiveKit CLI lk is bundled as the client tool for minting access tokens and calling the server API.

Security by design: no shipped keys and a per-instance API secret. The API key and secret sign the JWT access tokens that authorize room joins, so anyone holding the secret can mint tokens for any room. This image therefore never ships a usable key or secret.

On the first boot of every VM, the image generates a fresh per-instance API key and secret into the key file on the data disk. It also generates a fresh per-instance self-signed TLS certificate with the VM public IP in its subject alternative names, and writes the credentials to /root/livekit-credentials.txt. No two VMs share key material, and nothing usable exists in the captured image.

TLS for WebRTC out of the box. WebRTC signalling needs a secure context. nginx terminates TLS on port 443 and reverse-proxies the signalling WebSocket and the server API to the LiveKit server, which binds to loopback only. Port 80 redirects to HTTPS, and an unauthenticated /healthz endpoint answers Azure Load Balancer probes. Media flows directly to the RTC ports.

What you get:

  • LiveKit 1.13 (current stable release), the official prebuilt livekit-server Go binary, run under systemd as a dedicated unprivileged livekit system user
  • The LiveKit CLI lk bundled at /usr/local/bin/lk for minting access tokens and calling the RoomService API
  • nginx terminating TLS on port 443 (self-signed certificate regenerated per VM on first boot) and reverse-proxying the signalling WebSocket and API to the LiveKit server on loopback 127.0.0.1:7880, with port 80 redirecting to HTTPS and an unauthenticated /healthz probe
  • A per-instance API key and secret generated on first boot (no shared or shipped keys), stored in a key file on a dedicated 20 GiB Azure data disk at /var/lib/livekit
  • WebRTC media on a single TCP port 7881 and a single UDP mux port 7882, which keeps the firewall small, with use_external_ip so the server advertises the VM public IP for ICE
  • Ubuntu 24.04 LTS base with the latest security patches applied at build time, and unattended security upgrades kept enabled
  • 24/7 SecureAnyCloud support with a guaranteed 24-hour response SLA

Prerequisites

  • An active Azure subscription, an SSH public key, and a VNet and subnet in the target region
  • A subscription to the LiveKit listing on Azure Marketplace
  • Network Security Group rules that allow TCP 22 (admin), TCP 443 (signalling over TLS), TCP 7881 and UDP 7882 (WebRTC media) from the clients that will connect

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) for evaluation and small rooms. Production media workloads with many concurrent participants should use Standard_D4s_v5 or larger, and scale out with Redis and multiple nodes.

1

Deploy from the Azure Portal

Search for LiveKit in Marketplace, select the SecureAnyCloud publisher, and choose Create.

Configure the Network Security Group to allow TCP 443 (signalling), TCP 7881 and UDP 7882 (media) from your client networks, and TCP 22 for administration.

2

Deploy from the Azure CLI

RG="livekit-prod"; LOCATION="eastus"; VM_NAME="livekit1" IMAGE_URN="sac1647283583153:livekit-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:livekit-ubuntu-24-04:default:latest
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$IMAGE_URN" \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values "$SSH_KEY" \
  --public-ip-sku Standard
# Open signalling, media and admin ports on the VM NSG:
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 443 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 7881 --priority 1002
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 7882 --priority 1003
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1004

LiveKit media uses TCP 7881 and UDP 7882 by default. Make sure the rule for port 7882 uses the UDP protocol.

3

First boot

On first boot, the image resolves the VM public IP and generates a fresh per-instance API key and secret into the key file. It regenerates the self-signed TLS certificate with the VM public IP in its subject alternative names, brings up the livekit-server and nginx services, proves a token round trip works end to end, and writes /root/livekit-credentials.txt. This completes within a minute.

Connect over SSH as azureuser and read the summary.

sudo cat /root/livekit-credentials.txt

The summary records this VM's public IP, the signalling WebSocket URL (wss://<public-ip>/), the API key and the API secret. Treat the secret like a private key.

4

Confirm the LiveKit server is running

Confirm that the livekit-server and nginx services are both active. The signalling and API endpoint binds to loopback only on port 7880. The media ports listen on 7881/tcp and 7882/udp, and nginx listens on 443.

systemctl is-active livekit-server.service nginx.service /opt/livekit/livekit-server --version ss -tlnp | grep -E ':7880 |:7881 |:443 ' ss -ulnp | grep ':7882 '

Check the health endpoint that Azure Load Balancer probes use. It is unauthenticated and returns HTTP 200. Plain HTTP on port 80 redirects to HTTPS.

curl -s -o /dev/null -w 'healthz: %{http_code}\n' --cacert /etc/nginx/tls/livekit.crt https://127.0.0.1/healthz
curl -s -o /dev/null -w 'port 80: %{http_code}\n' http://127.0.0.1/
5

Inspect the config and the per-instance keys

The server configuration lives at /etc/livekit/config.yaml (constant, non-secret structure). The per-instance API key and secret live in a separate key file on the dedicated data disk at /var/lib/livekit/keys.yaml, generated on this VM's first boot. The key file is not world-readable, and the credentials file is root-only. Inspect the configuration, the data disk and the file permissions.

sudo grep -vE '^\s*#|^\s*$' /etc/livekit/config.yaml
sudo findmnt -no SOURCE,TARGET,FSTYPE,SIZE /var/lib/livekit
sudo stat -c '%n %a %U:%G' /var/lib/livekit/keys.yaml /root/livekit-credentials.txt

The real key file contains the live per-instance secret. That is why it is stored with restricted permissions and never shipped in the image.

6

Mint an access token and prove the round trip

Clients join a room with a signed JWT access token. Mint one with the bundled lk CLI, using the per-instance API key and secret, then validate it against the server.

A token signed with the real secret validates (HTTP 200). A token signed with a wrong secret is rejected (HTTP 401), which proves authentication is enforced.

sudo bash -c ' export HOME=/root CRED=/root/livekit-credentials.txt; CA=/etc/nginx/tls/livekit.crt K=$(grep "^livekit.api_key=" $CRED | cut -d= -f2-) S=$(grep "^livekit.api_secret=" $CRED | cut -d= -f2-) VT=$(lk token create --join --room demo --identity alice --valid-for 1h --api-key "$K" --api-secret "$S" --token-only 2>/dev/null | grep -m1 -oE "eyJ[A-Za-z0-9_-]+.eyJ[A-Za-z0-9_-]+.[A-Za-z0-9_-]+") echo -n "valid token -> HTTP "; curl -s -o /dev/null -w "%{http_code}\n" --cacert $CA "https://127.0.0.1/rtc/validate?access_token=$VT" BT=$(lk token create --join --room demo --identity mallory --api-key "$K" --api-secret wrong-secret --token-only 2>/dev/null | grep -m1 -oE "eyJ[A-Za-z0-9_-]+.eyJ[A-Za-z0-9_-]+.[A-Za-z0-9_-]+") echo -n "wrong secret -> HTTP "; curl -s -o /dev/null -w "%{http_code}\n" --cacert $CA "https://127.0.0.1/rtc/validate?access_token=$BT" '

You can also drive the RoomService API with the same credentials. Point the CLI at the server, trust the self-signed certificate, and list rooms.

sudo bash -c ' export HOME=/root CRED=/root/livekit-credentials.txt K=$(grep "^livekit.api_key=" $CRED | cut -d= -f2-) S=$(grep "^livekit.api_secret=" $CRED | cut -d= -f2-) export LIVEKIT_URL=https://127.0.0.1/ LIVEKIT_API_KEY=$K LIVEKIT_API_SECRET=$S export SSL_CERT_FILE=/etc/nginx/tls/livekit.crt lk room create demo-room >/dev/null lk room list lk room delete demo-room >/dev/null '
7

The signalling WebSocket and RTC handshake

The server startup log shows it resolving the VM public IP for ICE and starting on the configured ports. The signalling WebSocket at wss://<public-ip>/rtc upgrades a connection with a valid token to HTTP 101, which is how LiveKit client SDKs connect. Read the startup log.

sudo journalctl -u livekit-server.service --no-pager | grep -E 'starting LiveKit server|external IP|NAT1To1' | tail -4

Issue the certificate for your public address

First boot puts the address Azure reports into the TLS certificate, the credentials file and the login banner. With a Standard SKU public IP (the Azure default, and the --public-ip-sku Standard in Step 2), the current image records the VM's private address (10.0.0.x). Then livekit.ws_url points at an address your clients cannot reach, and SDKs refuse the certificate at the public IP.

Check which address was recorded.

sudo grep -E '^livekit.(host|ws_url)=' /root/livekit-credentials.txt

If it shows a 10. address, issue a new certificate for the public IP on the VM. Azure's instance metadata service reports the public IP to the VM itself, and nothing leaves the VM.

PUBLIC_IP=$(curl -s -H Metadata:true 'http://169.254.169.254/metadata/loadbalancer?api-version=2020-10-01' | grep -o '"frontendIpAddress":"[^"]*"' | head -1 | cut -d'"' -f4)
PRIV=$(hostname -I | awk '{print $1}'); echo "public $PUBLIC_IP, private $PRIV"
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout /etc/nginx/tls/livekit.key -out /etc/nginx/tls/livekit.crt \
  -subj "/C=GB/O=sac/CN=$PUBLIC_IP" \
  -addext "subjectAltName=IP:127.0.0.1,DNS:localhost,DNS:$(hostname -s),IP:$PRIV,IP:$PUBLIC_IP" sudo chmod 0600 /etc/nginx/tls/livekit.key
sudo sed -i "s/\b$PRIV\b/$PUBLIC_IP/g" /root/livekit-credentials.txt /etc/update-motd.d/90-livekit
sudo systemctl restart nginx

For a DNS name, set PUBLIC_IP to the name and write DNS:$PUBLIC_IP in place of IP:$PUBLIC_IP. Your API key and secret do not change, and media is unaffected, because livekit-server finds its public IP for ICE by itself. Copy the new /etc/nginx/tls/livekit.crt to development clients that trust it.

8

Connect an application

Use a LiveKit SDK (JavaScript, Swift, Kotlin, Go, Python and more) in your application. Mint a join token server-side with your API key and secret, then connect the client to the signalling URL.

// server side: mint a token with the LiveKit server SDK import { AccessToken } from 'livekit-server-sdk'; const at = new AccessToken('<api-key>', '<api-secret>', { identity: 'alice' }); at.addGrant({ roomJoin: true, room: 'my-room' }); const token = await at.toJwt(); // client side: connect with the token import { Room } from 'livekit-client'; const room = new Room(); await room.connect('wss://<your-vm-public-ip>/', token);

For development, you can trust the per-instance self-signed certificate at /etc/nginx/tls/livekit.crt. For production, replace it with a certificate signed by a public CA for your own domain.

9

Harden for production

  • Protect the API secret. It signs every access token. Keep it server-side, mint tokens with the shortest practical lifetime, and rotate the key file if it is ever exposed.
  • Use a real certificate. Replace the per-instance self-signed certificate with one signed by a public CA for your own domain, so browsers and SDKs trust the signalling endpoint without extra configuration.
  • Restrict the NSG. Allow TCP 443, TCP 7881 and UDP 7882 only from the networks that need them, and keep TCP 22 limited to your admin range.
  • Scale out. A single node handles a limited number of concurrent participants. For production scale, add Redis and run multiple LiveKit nodes behind a load balancer. For very high concurrency, consider a wider UDP port range in place of the single mux port.
  • Keep the data safe. The config and keys live on the data disk at /var/lib/livekit. Snapshot the disk as part of your DR plan.

Support

SecureAnyCloud provides 24/7 support with a guaranteed 24-hour response SLA. Contact sales@secureanycloud.com.

LiveKit is a trademark of LiveKit, Inc. SecureAnyCloud is not affiliated with or endorsed by LiveKit. This image ships the open-source LiveKit server (Apache 2.0). All product and company names are trademarks or registered trademarks of their respective holders and are used only to identify the software.

Deploy on Azure

Launch LiveKit 1.13 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure