LiveKit on Ubuntu 24.04 on Azure User Guide
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-serverGo binary, run under systemd as a dedicated unprivilegedlivekitsystem user -
The LiveKit CLI
lkbundled at/usr/local/bin/lkfor 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 loopback127.0.0.1:7880, with port80redirecting to HTTPS and an unauthenticated/healthzprobe -
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
7881and a single UDP mux port7882, keeping the firewall small, withuse_external_ipso 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 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 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
'

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

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.