vibe-live docs

Deploying vibe-live: LiveKit as the engine

LiveKit instead of OvenMediaEngine, in the same two shapes the other engines have (F59). Everything in all-in-one.md about required variables, sign-in, data and validating the install applies here too and is not repeated. What differs is the media half.

What it runs When to use it
all-in-one-livekit image the app, livekit-server, Ingress, Egress and Redis, supervised by s6 in one container one host, one thing to run -- an LXC, a single VM, a homelab
compose.livekit.yaml the same pieces as separate containers from LiveKit's official images, plus caddy and valkey when you want to restart, pin or resource-limit each piece on its own

The image is published by CI as livekit-<version> (and livekit-<commit sha>, livekit-latest) since 2026-09-28, or built with docker build --target all-in-one-livekit -t vibe-live:livekit .. compose.livekit.yaml builds its own app container from the all-in-one-ffmpeg target -- no OvenMediaEngine in this stack, and ffmpeg is part of the app -- which CI also publishes, as ffmpeg-.

Both were proved on 2026-09-14 by publishing to them with a real RTMP encoder: the channel went live in about two seconds, and the thumbnail and the 720p and audio renditions were served. Both give the same features as the OvenMediaEngine shape: RTMP from OBS with the channel's stream key, Go Live from the browser (a LiveKit room token, not WHIP), sub-second WebRTC for viewers, the stream menu with the owner's renditions and Audio only, Cast, thumbnails on the cards, viewer counts, and force-stop.

How the pieces fit

Ports

Why Symptom when it is closed
80/tcp, 443/tcp the site, and the ACME challenge (compose) as in all-in-one.md
1936/tcp RTMP from encoders the settings page shows an address that refuses connections
10000/udp WebRTC media streams connect and deliver no video
3478/tcp WebRTC over TCP, for networks that block UDP those viewers fall back to HLS; those streamers cannot go live from the browser

The same five ports as the OvenMediaEngine shape, with the same jobs. LiveKit muxes every peer connection onto its one udp_port rather than a port range, and runs ICE over TCP on 3478 where OvenMediaEngine runs its TCP relay; neither is HTTP, so neither can come through the app. LiveKit's own 7880 (signalling and room API) is never published: browsers reach /rtc through the app. Until 2026-09-27 this shape needed 7881/tcp and the 50000-50100/udp range instead, and a route for /rtc in your proxy.

The app also listens on 1937/tcp, never published: the forwarder the engine pushes a multistream (Twitch, YouTube) through, re-published by the app over RTMPS (VIBE_LIVE_FORWARD_LISTEN_ADDR, VIBE_LIVE_FORWARD_URL, loopback by default). On LiveKit, Egress makes the push.

Configuration

This is required in addition to all-in-one.md's list:

livekit-server generate-keys   # VIBE_LIVE_LIVEKIT_API_KEY and ..._SECRET
Variable Notes
VIBE_LIVE_LIVEKIT_URL What browsers join. Leave it unset: they then join on the site's own origin, through the app's /rtc proxy. Set it only for a LiveKit browsers reach elsewhere, and then /rtc there is yours to route.
VIBE_LIVE_LIVEKIT_API_KEY, VIBE_LIVE_LIVEKIT_API_SECRET One pair, read by the app and every LiveKit piece. The secret must be at least 32 bytes -- the app refuses to sign with less, and every piece refuses to start without the pair.

Everything else has a default that is the working configuration, in both shapes: the relay to Ingress, Egress on, the media directory, and the app's own address for LiveKit's API. Each can still be overridden:

VIBE_LIVE_OME_* do nothing here and the compose file does not pass them.

Seeking through a live stream (on unless an admin turns it off) works with Egress on (VIBE_LIVE_LIVEKIT_EGRESS=true): each live channel then runs one more Egress, recording its source into VIBE_LIVE_MEDIA_DIR/recordings for as long as it is live. Egress cannot copy a stream, so that is one encode per live channel for as long as it is live, at the largest quality in VIBE_LIVE_RENDITIONS -- budget it like one more rendition that is always watched, and size the directory at that quality's bitrate times the streams' length (2.5 Mbit/s for an hour is about 1.1 GB per channel), or cap it with VIBE_LIVE_DVR_WINDOW. Without Egress there is nothing to seek through, the app says so at start, and viewers are not offered it.

Past broadcasts use the same recording Egress, so they need Egress too. On LiveKit they are written to VIBE_LIVE_MEDIA_DIR/recordings unless VIBE_LIVE_RECORDINGS_DIR says otherwise -- it has to be a path Egress writes at as well, which is why the default is inside the media directory rather than beside the database. In compose that directory is a volume; keep it to keep them.

Host directories instead of volumes. VIBE_LIVE_DATA_HOST_DIR (the app's /data, its database) and VIBE_LIVE_MEDIA_HOST_DIR (the media directory the app and Egress share) in .env put either in a directory on the host rather than a named volume. The media one must be writable by Egress's user, uid 1001: sudo chown -R 1001:1001 <dir>.

What is inside the image, and what is not

Egress and CPU

Egress admits a request only while it estimates it has the CPU for it, so on a small host a rendition somebody asks for can be refused while others run. That is Egress's own admission, not the app's cap (VIBE_LIVE_MAX_TRANSCODES is the built-in engine's). The app retries a refused rendition on the viewer's next request and counts it in vibe_live_rendition_start_failures_total. Choosing fewer renditions in VIBE_LIVE_RENDITIONS is the lever.