Recording without Docker: LiveKit egress as a plain process
Castalong's per-guest recording is LiveKit egress: when you hit record, an egress worker joins the room as a hidden participant and writes each guest to their own MP4. LiveKit ships egress exclusively as a Docker image, and the received wisdom is that it can't run any other way — it launches headless Chrome, which wants CAP_SYS_ADMIN and a 1 GB /dev/shm. On our platform, Docker workloads don't autoscale: a Docker service bills its full resource ceiling around the clock. Recording is bursty — idle most of the day, busy for an hour — so the recorder was our most expensive service while doing nothing.
Chrome is only for compositing
Reading the egress source instead of the deployment docs changes the picture. Egress has several request types, and only room composite and web egress use Chrome: they render a browser page of the whole room and capture it. Participant egress — the only mode Castalong uses, one clean file per guest — is a pure SDK path: subscribe to the guest's tracks over WebRTC, decode, re-encode through GStreamer, mux an MP4. No browser, no Xvfb, no PulseAudio, no privileges. The whole reason for the container evaporates.
LiveKit publishes no binaries, so "just run the process" means building from source. The Go side is ordinary CGo. The system side is GStreamer, and Ubuntu 24.04 ships the same 1.24 series the official image builds — every element the participant pipeline needs is in the stock packages. Except one.
The one plugin Ubuntu won't ship
Egress encodes MP4 audio with the faac GStreamer element. Debian and Ubuntu have never shipped it — the faac codec's license rider keeps it out of the archive (a bug open since 2014). Without it, every recording dies at pipeline construction with a missing-element error.
The fix is small once you know it: build that single element out of the gst-plugins-bad source that matches the distro's GStreamer, and ship the resulting libgstfaac.so next to the binary:
meson setup b -Dauto_features=disabled -Dfaac=enabled -Dgpl=enabled
ninja -C b ext/faac/libgstfaac.so
# at runtime:
export GST_PLUGIN_PATH=/path/to/that/dir
One .so, a few seconds of compile, and the distro packages cover everything else.
Two traps the image was hiding
The Docker image quietly papers over two hard assumptions in egress itself:
- A hardcoded scratch dir.
TmpDiris the constant/home/egress/tmp— not configurable. The image ships that directory pre-made; a plain process on a locked-down host dies withmkdir /home/egress: permission denied. Create it at image-prepare time and move on. - The binary must be named
egress. The server doesn't run jobs in-process: for every request it spawns a handler by exec'ingegressfrom$PATH. Name your build anything else and you get the most misleading failure of the whole exercise — a worker that starts cleanly, reports ready, accepts the job, and then fails it withexecutable file not found in $PATH.
Neither is documented, because inside the image neither can ever go wrong.
What it buys
As a plain process the recorder autoscales like everything else: a small idle allocation that grows cores while encoding and falls back after — roughly a quarter of the fixed Docker price, so recording stays on all the time instead of being started for sessions. One config nudge matters: egress admission-controls by CPU cost per request type, and the stock participant cost of 2 makes a small idle worker refuse jobs before the autoscaler has any load to react to. Costs are configurable; measure your own pipeline (ours is h264-in, mostly passthrough) and set them honestly.
Finished files don't touch shared storage either. Each egress request carries its own S3 output, so the worker uploads the MP4 itself and control reads it back over S3 — presigned redirects for downloads, streamed zips for bundles. The worker's disk is scratch space; its containers stay disposable.