Aller au contenu

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\LiveKitInterviewRoomProvider mints an HS256 JWT with a video grant. Bound in dev/prod; the test suite uses FakeInterviewRoomProvider.
  • 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 match livekit.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.

  1. Host: Scaleway instance (French region). Install Docker (or the LiveKit binary).
  2. Config (livekit.yaml), production differences from the dev config:
  3. rtc.use_external_ip: true and advertise the public IP.
  4. Open the RTC UDP port range (e.g. 50000-60000/udp) plus 7881/tcp on the firewall.
  5. TLS: terminate WSS on a reverse proxy (Caddy/nginx) on a WonderWork domain (e.g. interview.wonderwork.fr), proxying to :7880.
  6. 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.
  7. Keys: provide keys from the environment secret, key file at 0600.
  8. Webhook: set webhook.api_key (signing) and webhook.urls to 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 from participant_joined events. The endpoint is public at the firewall (no WonderWork session), authenticated solely by that signature, so the signing key must match LIVEKIT_API_SECRET.
  9. room.auto_create: true; recording/egress left disabled.
  10. API env (Symfony Secrets / Vault, never committed):
  11. LIVEKIT_URL=wss://interview.wonderwork.fr
  12. LIVEKIT_API_KEY / LIVEKIT_API_SECRET — per environment, generated with docker run --rm livekit/livekit-server:v1.13.6 generate-keys (or any random pair).
  13. The adapter fails fast if any of the three is missing.
  14. Health / monitoring: point the uptime check at GET / (returns OK).
  15. 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.