Castalong
← Engineering notes

Recording without Docker: LiveKit egress as a plain process

InfrastructureJuly 2026

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:

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.

The container wasn't protecting anything — it was hiding three requirements: one missing distro plugin, one hardcoded path, one magic binary name. Meet those and egress is just a Go process with GStreamer, and it scales like one.
← Engineering notes