L1
Applications Azure

LiveKit on Ubuntu 24.04 on Azure User Guide

| Product: LiveKit 1.13 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of LiveKit on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. 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 realtime 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 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 to run it. 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, a per instance API secret. The API key and secret sign the JWT access tokens that authorise 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 very first boot of every VM a fresh per instance API key and secret are generated into the key file on the data disk, a fresh per instance self signed TLS certificate is generated with the VM public IP in its subject alternative names, and the credentials are written to /root/livekit-credentials.txt. No two VMs ever share key material, and nothing usable exists in the captured image.

TLS for WebRTC out of the box. WebRTC signalling needs a secure context, so 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 is included:

  • 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, keeping the firewall small, with use_external_ip so the server advertises the VM public IP for ICE

  • Ubuntu 24.04 LTS base with latest security patches applied at build time, and unattended security upgrades kept enabled

  • 24/7 cloudimg support with a guaranteed 24 hour response SLA

Prerequisites

  • Active Azure subscription, SSH public key, VNet and subnet in target region

  • Subscription to the LiveKit listing on Azure Marketplace

  • Network Security Group rules allowing 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.

Step 1: Deploy from the Azure Portal

Search LiveKit in Marketplace, select the cloudimg publisher, and click 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.

Step 2: Deploy from the Azure CLI

RG="livekit-prod"; LOCATION="eastus"; VM_NAME="livekit1"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/livekit-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
  --resource-group "$RG" --name "$VM_NAME" \
  --image "$GALLERY_IMAGE_ID" \
  --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 UDP rule uses the UDP protocol.

Step 3: First boot

On first boot the image resolves the VM public IP, generates a fresh per instance API key and secret into the key file, 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. SSH in as azureuser and read the summary:

sudo cat /root/livekit-credentials.txt

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

Step 4: Confirm the LiveKit server is running

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 '

The livekit-server and nginx services both active under systemd, the systemctl status showing livekit-server loaded and running, the livekit-server version 1.13.3, and the listener table showing the signalling API bound to loopback 127.0.0.1 on port 7880, the RTC media ports on 7881 tcp and 7882 udp, and nginx terminating TLS on port 443

The unauthenticated health endpoint used by Azure Load Balancer probes returns HTTP 200, and 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/

Step 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 first boot. The key file is not world readable and the credentials file is root only:

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 livekit config yaml showing port 7880 bound to loopback 127.0.0.1, the rtc section with tcp_port 7881, udp_port 7882 and use_external_ip true, and the key_file pointing at the data disk, then the per instance keys.yaml on the dedicated data disk mounted at /var/lib/livekit showing the API key with the secret redacted for display because it is unique per VM and never shown or shipped, and the file modes showing the key file others bit is 0 and the credentials file is 0600 root only

The API secret is redacted in the screenshot above. The real key file contains the live per instance secret, which is why it is stored with restricted permissions and never shipped in the image.

Step 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), and 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
'

Minting a JWT access token with the per instance API key and secret using the LiveKit CLI, the server validating a token signed with the per instance secret and returning HTTP 200 success, a wrong secret token being rejected with HTTP 401 which proves authentication is enforced, and the RoomService management API accepting the per instance credential to create and list a room

Step 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:

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

The livekit-server startup log showing it resolving the external IP via STUN and starting the LiveKit server on port 7880 with the RTC ports 7881 and 7882, the unauthenticated health probe on nginx 443 returning HTTP 200 and plain HTTP on port 80 returning 301, and the signalling WebSocket handshake on wss rtc upgrading to HTTP 101 Switching Protocols with the Sec WebSocket Accept header

Step 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.

Step 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, and consider a wider UDP port range in place of the single mux port for very high concurrency.

  • 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

cloudimg provides 24/7 support with a guaranteed 24 hour response SLA. Contact support@cloudimg.co.uk.

LiveKit is a trademark of LiveKit, Inc. cloudimg 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.