Runbook — Video interview media server (LiveKit, self-hosted)¶
Integrated video interviews run on a self-hosted LiveKit SFU. There is no LiveKit Cloud account and no third-party API key: the server is our own, the access tokens are signed locally by the API, and all media stays on French infrastructure (RGPD / CLOUD Act).
- Adapter:
App\Service\Interview\LiveKitInterviewRoomProvidermints an HS256 JWT with avideogrant. Bound in dev/prod; the test suite usesFakeInterviewRoomProvider. - Server version:
livekit/livekit-server:v1.13.6(pin on upgrade). - 1-to-1 cap: enforced by the API-side identity restriction (only the two parties get a token). LiveKit has no global per-room participant limit.
Local development¶
LiveKit runs as the livekit service in docker-compose.yml (always-on, dev + e2e).
- Config:
docker/livekit/livekit.yaml(dev key/secret, RTC ports, no TLS/TURN). - API env (
api/.env, dev defaults): LIVEKIT_URL=ws://localhost:7880(the URL handed to the browser)LIVEKIT_API_KEY=devkey/LIVEKIT_API_SECRET=devsecret…(must matchlivekit.yaml)- Start:
docker compose up -d livekit - Health:
curl http://localhost:7880/→OK(HTTP 200)
Smoke test — token acceptance¶
Verifies the API-minted token structure is accepted by a real server (no full media needed):
TOKEN=$(docker compose exec -T api php -r '
$k="devkey";$s="devsecretdevsecretdevsecretdevsecret";
$b=function($d){return rtrim(strtr(base64_encode($d),"+/","-_"),"=");};
$h=$b(json_encode(["alg"=>"HS256","typ"=>"JWT"]));
$p=$b(json_encode(["iss"=>$k,"sub"=>"u","nbf"=>time(),"exp"=>time()+3600,"name"=>"Test","video"=>["room"=>"r","roomJoin"=>true,"canPublish"=>true,"canSubscribe"=>true]]));
echo "$h.$p.".$b(hash_hmac("sha256","$h.$p",$s,true));')
curl -s "http://localhost:7880/rtc/validate?access_token=$TOKEN" # → success
A malformed token returns invalid authorization token (HTTP 401).
Production / staging deployment (Scaleway, ops)¶
Deploy on a compute instance / VM (not a UDP-restricted managed container platform), one per environment (production and staging), each with its own domain and secrets.
- Host: Scaleway instance (French region). Install Docker (or the LiveKit binary).
- Config (
livekit.yaml), production differences from the dev config: rtc.use_external_ip: trueand advertise the public IP.- Open the RTC UDP port range (e.g.
50000-60000/udp) plus7881/tcpon the firewall. - TLS: terminate WSS on a reverse proxy (Caddy/nginx) on a WonderWork domain
(e.g.
interview.wonderwork.fr), proxying to:7880. - TURN: enable the embedded TURN server with its own domain + TLS certificate and its relay port range opened — required so candidates behind restrictive NAT / mobile networks can connect.
- Keys: provide
keysfrom the environment secret, key file at0600. - Webhook: set
webhook.api_key(signing) andwebhook.urlsto the API's public webhook endpoint (https://<api-host>/webhooks/livekit). The API verifies the LiveKit signature on the raw body and derives interview attendance fromparticipant_joinedevents. The endpoint is public at the firewall (no WonderWork session), authenticated solely by that signature, so the signing key must matchLIVEKIT_API_SECRET. room.auto_create: true; recording/egress left disabled.- API env (Symfony Secrets / Vault, never committed):
LIVEKIT_URL=wss://interview.wonderwork.frLIVEKIT_API_KEY/LIVEKIT_API_SECRET— per environment, generated withdocker run --rm livekit/livekit-server:v1.13.6 generate-keys(or any random pair).- The adapter fails fast if any of the three is missing.
- Health / monitoring: point the uptime check at
GET /(returnsOK). - Sovereignty: the reverse proxy, LiveKit, and TURN all run on the French instance; no component of the media path leaves French/EU infrastructure.
Secret rotation¶
Rotate LIVEKIT_API_KEY/SECRET by adding the new pair to livekit.yaml keys (LiveKit
accepts multiple keys), deploying the new secret to the API, then removing the old key on the
next deploy. No downtime.
Full media smoke (post-deploy, manual)¶
Two clients on different networks (one behind a restrictive NAT / on mobile) join the same room and exchange audio + video — this validates the RTC port range and the TURN relay path.
Operations¶
- Logs:
docker compose logs livekit(local) /journalctl/container logs (host). - Restart:
docker compose up -d livekit(local) / restart the service (host). - Config location:
docker/livekit/livekit.yaml(local);/etc/livekit/livekit.yaml(host). - Upgrade: bump the pinned image tag, re-run the token-acceptance smoke, then the media smoke.