Self-hosting Open Live
Open Live is open source and you can run the whole thing yourself without paying us anything. This guide covers what the system is made of and under which licences, how to stand it up, every environment variable that matters, the one security decision self-hosting hands you, and an honest account of what you take on. It is written so you can make the build-or-buy call on real detail rather than on a marketing page.
Why we publish this. The reason to run Open Live on Open Source Cloud should be that operating it yourself is not worth your time, not that leaving is difficult. If this guide talks you into self-hosting, that is a legitimate outcome.
What it is made of
| Component | What it does | Source | Licence |
|---|---|---|---|
| open-live | The API server. Productions, sources, templates, and the WebSocket controller channel. | Eyevinn/open-live | MIT |
| open-live-studio | The browser production controller. | Eyevinn/open-live-studio | MIT |
| Strom | The GStreamer flow engine that does the actual video work. This is the part that needs the GPU. | Eyevinn/strom | Dual MIT / Apache-2.0 |
| CouchDB | Stores productions, sources and templates. | Upstream project | Apache-2.0 |
Strom carries both LICENSE-MIT and
LICENSE-APACHE, the usual Rust dual-licence, so you may
take it under either. Do not repeat the shorthand that it is
Apache-only; that is what GitHub's single-licence field displays, not
what the repository grants.
Requirements
-
pnpm 10.33 or later, corroborated by
packageManager: pnpm@10.33.0inpackage.json. For Node, both readmes say 23 or later, butpackage.jsondeclares noenginesfield and pins@types/node: ^22.15.3, so 23 rests on the readme alone. Treat it as the stated minimum rather than an enforced one, and checkpackage.jsonbefore provisioning. - A CouchDB instance, local or remote.
- A Strom instance the API server can reach. Strom is where the GPU requirement lives, not in the API server.
Standing it up locally
The API server repository ships a reference
docker-compose.yml that brings up CouchDB 3 alongside
open-live, which is the fastest way to see the thing run. From source:
pnpm install
cp .env.example .env
# fill in the values, then
pnpm dev # hot reload
pnpm typecheck # type-check without emitting
pnpm build # compile to dist/
pnpm start # run the compiled server
The REST API is documented in docs/openapi.yaml and
served from the running instance at /documentation. The
WebSocket controller channel has its own document in the repository,
covering authentication, inbound message types and outbound
broadcasts.
Configuration
Read from src/config.ts and src/main.ts
rather than from the readme, which is stale on several of these.
| Variable | Purpose | Default |
|---|---|---|
PORT |
Port the API server listens on | 3000 |
COUCHDB_URL |
CouchDB connection URL. Credentials may be embedded, or supplied
separately via COUCHDB_USER and
COUCHDB_PASSWORD, which are merged in when the URL
carries no password of its own.
|
required |
PUBLIC_BASE_URL |
The externally reachable URL of this service. A hard production startup guard and a security control, not a convenience setting. See the warning below. |
unset, and the server refuses to start without
it under NODE_ENV=production
|
API_KEY |
Static key protecting /api/v1 routes and the
WebSocket controller. The second hard production guard.
|
unset |
TRUST_EXTERNAL_AUTH |
Declares that API_KEY is unset on purpose because
something in front authenticates. Only meaningful when
API_KEY is unset.
|
false |
SRT_PASSPHRASE_KEY |
Key used to encrypt SRT passphrases at rest. Fails closed in
production on the encrypt and decrypt path. Precision worth
keeping: this is not one of the two startup guards, so
a deployment without it starts and then fails the moment a
passphrase is handled. .env.example says the server
will not start without it; that is overstated.
|
unset |
CORS_ORIGIN |
Allowed origin or origins. Accepts a comma-separated list or
*.
|
unset, and unset means cross-origin is disabled, not opened. The server sets origin: false and
emits a [security] warning. A regression test
guards this so it cannot silently become permissive. The readme
and .env.example each state a different, wrong
default.
|
TRUSTED_HOSTS |
Comma-separated allow-list of hostnames permitted when a WHIP
callback URL is derived from the request because
PUBLIC_BASE_URL is unset. A request whose derived
host is not listed is rejected rather than persisted.
|
empty |
STROM_URL |
Base URL of the Strom pipeline engine | http://localhost:7000 |
STROM_AUTH_MODE |
osc exchanges a Personal Access Token for a
short-lived Service Access Token against OSC's token service.
direct uses the API key as a bearer token
directly, and is the mode a self-hosted Strom needs.
|
osc |
STROM_AUTH_TOKEN |
The token itself. STROM_TOKEN is still read as a
legacy fallback, but this is the current name.
|
unset |
LOG_LEVEL |
trace to error |
info |
STROM_PORT_LEASE_SIZE |
Consecutive SRT listener ports leased from a shared Strom | 20 |
STROM_PORT_LEASE_CLIENT_ID |
Override for the lease client id sent to Strom |
hostname of PUBLIC_BASE_URL, else
open-live-<hostname>
|
STROM_PORT_LEASE_DISABLED |
Skips port leasing entirely, for single-tenant Strom setups | false |
COUCHDB_NAME does nothing, whatever the readme
says.
The database name is hardcoded to open-live in
src/db/index.ts, and the variable appears nowhere in
the source. Setting it has no effect.
PUBLIC_BASE_URL is a security control.
Without it the activation endpoint derives WHIP endpoint URLs from
the X-Forwarded-Host request header, which an attacker
can forge to
redirect camera streams to a server they control.
That is why it is a hard startup guard in production rather than a
warning. If you deliberately leave it unset outside production, set
TRUSTED_HOSTS.
Authentication is the decision self-hosting hands you
When API_KEY is unset, every API route is
unauthenticated. Any client that can reach the service can create,
modify, delete and activate productions.
The project does not leave this to chance, and the behaviour is worth
knowing exactly. With NODE_ENV=production and no
API_KEY,
the server refuses to start unless
TRUST_EXTERNAL_AUTH=true declares that a trusted layer in
front is handling it. Outside production it logs a prominent warning
instead. The reference docker-compose.yml will not bring
the stack up without a key either.
The reasoning behind that pair is worth borrowing: the deployment tier
and the authentication architecture are different questions, so
NODE_ENV alone cannot stand in for the second one. On
Open Source Cloud the reverse proxy authenticates every request before
it reaches the API, which is the case
TRUST_EXTERNAL_AUTH exists for. Self-hosted, that proxy
is the part you are replacing.
Generate a real key rather than inventing one:
openssl rand -base64 32
HTTP clients send it as
Authorization: Bearer <API_KEY>. Note that
/health, /ready and
/api/v1/status stay exempt even when the key is set, so
your probes keep working.
WebSocket clients do NOT pass the key as ?key=. That
was removed on purpose and the server no longer accepts
it.
A static, non-expiring key in a URL leaks into proxy, CDN and
browser DevTools access logs as permanent credentials. Both the
readme and docs/controller-websocket.md still document
the old form; the source and its tests are the authority, and a
client built from the readme will get 401s.
The current mechanism works around the same browser limitation a
different way. A browser cannot set arbitrary headers on a WebSocket
handshake, but it can offer subprotocols. The client offers two: the
plain marker openlive.bearer, and
openlive.bearer.<key> carrying the key. The
server reads the key from the second and echoes back only the plain
marker, so the secret never appears in the handshake response header
either.
new WebSocket(url, ["openlive.bearer", `openlive.bearer.${apiKey}`])
Pointing the studio at your backend
The studio reads one setting, and it is not a
VITE_ variable. vite.config.ts sets
envPrefix: ['OPEN_LIVE_'], which replaces Vite's
default VITE_ prefix rather than adding to it, so a
VITE_-prefixed variable is never exposed to the bundle at
all. The variable is OPEN_LIVE_URL, and its default is
http://localhost:3000.
The runtime value wins, so you do not need to rebuild to repoint
it.
src/lib/base.ts resolves in this order:
window._env_?.OPEN_LIVE_URL || import.meta.env.OPEN_LIVE_URL || "http://localhost:3000"
docker-entrypoint.sh writes
window._env_ into env-config.js when the
container starts, and the non-Docker pnpm start path
does the same into dist/. Set
OPEN_LIVE_URL in the environment and restart; a rebuild
is not required on either path.
If the studio is talking to the wrong backend, check
env-config.js in the served directory before you reach
for a rebuild. Note also that
docker-entrypoint.sh requires jq and fails
closed without it, deliberately, rather than falling back to manual
escaping.
Kubernetes: what you get, and what you do not
The product is described on this site as deployable on any CNCF-conformant Kubernetes cluster. The repositories themselves say only "any Kubernetes cluster". It is worth being precise about what the repositories actually ship, because it is not a chart.
-
You get: Dockerfiles for open-live, the studio and
Strom, a reference
docker-compose.ymlfor local work, and an nginx template for serving the studio. - You do not get: a published Helm chart or ready-made Kubernetes manifests. Writing those is your side of the line.
The useful reference is the topology Open Source Cloud itself runs,
because it is documented and it works: CouchDB
(apache-couchdb), Strom (eyevinn-strom), and
a parameter store (eyevinn-app-config-svc with
valkey) injecting environment variables at runtime so no
.env file sits on the server. Four services, not one, and
the parameter store is the piece people forget when they plan this as
a two-container deployment.
What you are taking on
| Concern | Self-hosted | On Open Source Cloud |
|---|---|---|
| Licence cost | None. MIT and dual MIT/Apache-2.0. | None. Same software, same licences. |
| Authentication | You build and operate it, or you run with an open API. | Reverse proxy authenticates before the API. |
| GPU capacity | You buy, size and keep a GPU host alive. This is the real cost and the real operational burden. | Three options, and only two are for live production. The shared Frankfurt GPU on Professional is marked demo and evaluation only because capacity is not guaranteed. For live production it is either the Hosted GPU add-on, from 750 EUR/month, which our pricing section describes as guaranteed capacity though the platform pricing page lists no GPU add-on and no SLA is published behind the phrase, or bring your own Strom, which puts you back in the left column for this row. |
| CouchDB | Yours to run, back up and restore. | Provisioned by the create flow, password generated for you. |
| Kubernetes manifests | You write and maintain them. | Not applicable. |
| Upgrades | Yours to schedule, test and roll back. | Managed. |
| Leaving | Not applicable. | Same open source software, so the exit is a redeploy rather than a migration. |
The honest summary: the API server and the studio are ordinary Node services and are not hard to run. Strom and the GPU host are the real work, and so is being the person who fixes a pipeline at 19:55 on a Saturday.
Pre-production checklist
-
API_KEYis set, orTRUST_EXTERNAL_AUTHis set and something in front genuinely authenticates. Verify from outside your network, not from a machine on it. -
CORS_ORIGINmatches the origin the studio is actually served from. -
OPEN_LIVE_URLis set in the studio's environment andenv-config.jsin the served directory carries the value you expect. - CouchDB has backups, and a restore has been rehearsed rather than assumed.
- You have read the SRT passphrase rotation runbook before you need it.
- The GPU host has been load-tested at your real source count, not with two test feeds.
- Someone is named as on-call for the pipeline during a show, and it is not the person directing it.
Prefer not to operate it?
Same software, hosted and managed. Bring your own Strom alongside Personal at 15 EUR/month, evaluate on the shared Frankfurt GPU at 69 EUR/month, or take the Hosted GPU add-on from 750 EUR/month when you need guaranteed capacity for live production. Compare those against what you just read.