-
Notifications
You must be signed in to change notification settings - Fork 0
CONFIGURATION
Generated from
docs/CONFIGURATION.md. Edit that file and re-runnode scripts/publish-wiki.mjs --push. An edit made here is a fork of the documentation that nothing reconciles, and the next run of this script will overwrite it without asking.
The single most useful thing to understand about configuring polyemesis is where a setting lives, because there are two places and they are not interchangeable.
config.yaml (and flags) |
The web UI (stored in SQLite) | |
|---|---|---|
| What | Things that must be known before the database opens | Everything else |
| Examples | Listen address, data directory, TLS, FFmpeg paths | Ingest ports, destinations, routing, retention, credentials |
| Changing it | Edit the file, restart | Takes effect immediately |
| Where it lives | A file you manage | <dataDir>/polyemesis.db |
That is the whole rule. If a setting is needed to bring the server up, it is in the file. If it describes what the server should do, it is in the UI.
This is why there is no destinations: block in config.yaml, and why people
looking for one do not find it: destinations are runtime state, edited while the
server is running, and a config file would be a second source of truth for them.
Copy config.example.yaml to config.yaml and edit.
That file is the reference — every key is documented inline, with worked
examples for the five common TLS deployments. It is deliberately the canonical
copy so there is no second version of the truth to drift.
The keys, in brief:
| Key | Default | What it is |
|---|---|---|
addr |
":8080" |
HTTP listen address for the UI and API |
dataDir |
"./data" |
Holds polyemesis.db, secret.key, recordings/, hls/, tls/
|
tls.mode |
"off" — but config.example.yaml ships "auto"
|
auto, acme, selfsigned, manual, off
|
tls.hostname |
"" |
The DNS name this server is reached by |
tls.acmeEmail |
"" |
Required for acme; where expiry warnings go |
tls.certFile / tls.keyFile
|
"" |
manual mode only |
tls.hsts |
false |
Opt-in on purpose — see below |
trustProxyHeaders |
false |
Set only behind a reverse proxy you control |
ffmpeg.binary / ffmpeg.probe
|
"" |
Pin specific binaries instead of searching $PATH
|
Every flag overrides the file. Useful for containers and for trying something without editing anything.
-addr HTTP listen address
-config path to config.yaml (default "config.yaml")
-data data directory
-ffmpeg path to the ffmpeg binary
-ffprobe path to the ffprobe binary
-log debug | info | warn | error (default "info")
-version print the version and exit
-reset-admin set a new admin password and sign out every session, then exit
-reset-admin is for an operator who has shell access and no way in through the
UI. It asks twice without echoing, and it is safe to run against a live server:
it touches only the database and exits before anything binds a port. Piping the
password twice scripts it. See the FAQ for why deleting the row from the users
table is the wrong way to do this.
-log debug is the one to reach for when something is wrong. It logs each
child's full command line as it spawns, which is usually the fastest route to
understanding a process that will not start.
Those are two different things, and the difference is plain HTTP.
Start polyemesis with no tls: block at all — no config file, or a minimal
one — and it serves plaintext, because the compiled-in default is off. The
auto in the table above is what you get by starting from
config.example.yaml, which ships mode: "auto" uncommented. A new install
that follows the documented path is therefore encrypted; one assembled by hand
from the key list is not.
The server warns loudly at startup when it is binding publicly without TLS. That warning is the thing to read.
It means "pick the sensible mode for this deployment", and one of the outcomes
is off:
-
trustProxyHeaders: true→ off (your proxy is terminating TLS) - a public hostname and
acmeEmailset → acme - anything else → selfsigned
A "public hostname" is one Let's Encrypt could actually validate: it has a dot,
is not an IP address, and does not end in .local, .internal, .lan,
.home, .arpa or localhost. If you expected ACME and got a self-signed
certificate, the hostname is almost always why.
HSTS is remembered by the browser. Once it has seen the header for a hostname it
will refuse plain HTTP to that name until max-age elapses, and the server
cannot take it back. Turn it on when you have a publicly trusted certificate
and intend to keep one. It is ignored in selfsigned mode and when TLS is off —
both log a warning rather than failing, because pinning a certificate the browser
cannot validate is an excellent way to lock yourself out of your own tool.
It is no longer a key, and it does not need reinstating. Enhanced RTMP / multitrack FLV ingest (OBS 30.2+) turns out to work on FFmpeg 7.1+ — verified end to end — and not on FFmpeg 6.1.1, which is Ubuntu 24.04's stock build. Either way there is nothing for a flag to switch: where it works, it works by default, because the tracks arrive through the existing ingest command unchanged. It confirms rather than undermines the removal — the key was declared as a placeholder and kept on the belief that config files already carrying it would otherwise fail to parse.
That belief was wrong: config loading ignores unrecognised keys, so the
declaration was buying nothing while presenting a knob an operator could set and
watch have no effect. A config file that still names enhancedRtmp loads
exactly as before. For multitrack ingest that is actually operated, use SRT:
Enhanced RTMP's version dependency is real, and OBS does not send multitrack
audio over it — measured against OBS 30.2.3, which emitted only legacy
single-track tags. See evidence/enhanced-rtmp-multitrack.md.
Most of what makes polyemesis awkward to set up — browser warnings, and every platform refusing to accept a callback URL — is one problem wearing two hats: the server has no name anyone else recognises. Three config keys fix both at once, and the machinery is already here.
-
Point a DNS record at the box.
stream.example.com. A 203.0.113.10 -
Name it in
config.yaml.tls: mode: auto # or acme, to be explicit hostname: stream.example.com acmeEmail: you@example.com
-
Make port 443 reachable from the internet, including inbound from Let's Encrypt. See the validation note below — this is the step people skip.
-
Restart. Nothing happens yet, on purpose.
Startup does no network I/O. It resolves the mode (see
tls.mode: auto does not mean "always encrypt"),
then creates a certificate cache under the data directory, locked to this
account — it holds the ACME account key and every private key that is ever
issued.
Issuance happens on the first HTTPS handshake for the configured name. A browser arrives, there is no certificate yet, and polyemesis orders one then.
Let's Encrypt validates by connecting back to you, and polyemesis answers either of the two challenges it may choose:
- HTTP-01, on port 80. A small helper listener is started for this whenever TLS is enabled and the main listener does not already own the port. If it cannot bind, issuance keeps failing — the startup log calls this "the single most common reason Let's Encrypt issuance never completes".
- TLS-ALPN-01, on port 443. Answered on the listener already serving the UI, with no extra port.
Open both 80 and 443 inbound. Outbound-only firewalls and un-forwarded NAT fail here, and the symptom is a browser certificate warning that never clears rather than an obvious error. If port 80 is genuinely unavailable — a proxy owns it, or the process cannot bind low ports — polyemesis says so rather than leaving you to guess; see the certificate status in the UI.
The first request after a cold start is slow, because that is when issuance runs. On a machine you are about to broadcast from, load the page once before you need it.
Renewal is automatic and also happens on a handshake. The cache survives restarts, so this is a first-boot cost, not a recurring one.
Point the record at the box with the proxy off — the grey cloud, "DNS only":
stream.example.com A 203.0.113.10 DNS only
Proxying it (the orange cloud) breaks certificate issuance in both directions at once: Cloudflare terminates TLS, so the TLS-ALPN-01 challenge never reaches you, and it proxies and caches HTTP, so the HTTP-01 challenge is unreliable at best.
There is a second reason, and for a streaming box it is the decisive one: Cloudflare's proxy carries HTTP and HTTPS only. RTMP on 1935 and SRT over UDP cannot pass through it at all, so a proxied name cannot serve your ingest even when the certificate works.
If you specifically want the web UI behind the proxy, do not use ACME for it.
Issue a Cloudflare Origin Certificate, point tls.mode: manual at it with
certFile and keyFile, and set trustProxyHeaders: true so the callback URI
is built from the address the browser actually used. You will still need a
second, unproxied name for ingest.
polyemesis builds every OAuth callback from the address you reached it at. Browse to the real name and the callback becomes:
https://stream.example.com/api/v1/oauth/twitch/callback
That is a string a platform will accept. Paste it into each developer console — Twitch, YouTube, Facebook, Kick all take it, with no relay, no device-code flow and no second application to register.
This is the whole reason to bother. Connecting an account from a box reachable
only as 192.168.1.50, or on a self-signed certificate, does not work at any
platform, because there is no address their console will let you register.
You do not have to assemble that string yourself. Settings → the card for each
platform shows the exact URI to register, built from the address you are
browsing right now, with a copy button beside it. It is computed rather than
described because getting it wrong — a stray slash, http where the platform
demands https — is the most common reason a setup fails, and the error the
platform returns names none of that.
The same card checks the URI before you paste it, and will tell you when:
- the address is plain HTTP on a routable host, which Google refuses outright and Twitch allows only for loopback;
- the address is a bare IP, which Google will not accept for a web application client;
- you are browsing one name while
tls.hostnamesays another, which producesredirect_uri_mismatchat the platform; - a reverse proxy is in front but
trustProxyHeadersis off, so the URI on screen is not the one your browser actually used.
Loopback is deliberately exempt from the bare-IP warning: http://127.0.0.1 is
what the platforms' own documentation recommends for local development, and
warning about the configuration they recommend is how operators learn to click
past warnings.
Issuance is rate limited, so a wrong DNS record is expensive. polyemesis pins issuance to the single configured name for exactly this reason: anyone who can reach the port could otherwise make it request certificates for names you do not own and exhaust the budget. Check the record resolves before the first request, not after five failures.
trustProxyHeaders: true turns this off. It resolves to off, because it
means a reverse proxy is terminating TLS and issuing certificates is the
proxy's job. That is a supported setup; the callback URL then comes from the
X-Forwarded-Host and X-Forwarded-Proto headers your proxy sets, so it still
needs to be the public name.
A hostname without a dot, an IP literal, or a .local/.internal/.lan
suffix is not a public name and silently gives you a self-signed certificate
instead. If you expected ACME and did not get it, that is almost always why.
Renaming later is not free. The hostname ends up inside every redirect URI you have registered at every platform. Changing it means re-registering all of them, and existing connected accounts keep working only until their tokens need a refresh through the old callback. Pick the name you intend to keep.
Giving the box a real name is the durable answer. This is the one for a box that does not have a name yet — a machine on your LAN, a rented server you are still evaluating, or anything you reach over SSH.
It needs no certificate, no DNS record and no changes to polyemesis. It works because every platform makes an exception for loopback addresses, and because polyemesis builds its callback from the address you reached it at.
Bind to loopback only, and let SSH carry the encryption:
addr: "127.0.0.1:8080"
tls:
mode: "off"Then, from the machine you browse on:
ssh -L 8080:127.0.0.1:8080 you@your-server
and open http://localhost:8080.
Turning TLS off is safe only in this shape. The listener is not reachable
from the network at all — SSH is the encrypted channel, and it is doing the same
job the "put a reverse proxy in front" advice describes. Do not combine
tls.mode: off with a public bind address; polyemesis warns loudly at startup
if you do, because that puts passwords and session cookies on the wire in the
clear.
Settings shows the exact URI for each platform, with a copy button. Browsing at
localhost:8080, it will read:
http://localhost:8080/api/v1/oauth/twitch/callback
Twitch — paste it as an OAuth Redirect URL. Twitch permits plain HTTP for
loopback and nothing else, so this is the one case where http:// is correct.
YouTube — create the OAuth client with application type Desktop app, not
Web application, and register the same URL with /youtube/. Google requires the
redirect to match exactly including the port, which is easy here: polyemesis
serves a fixed port from addr, so unlike a desktop application there is no
ephemeral port to reconcile.
Google will show an "unverified app" screen until the app is verified. For a personal install, keeping the app in testing mode and adding yourself as a test user is enough.
For YouTube it is worth being explicit: Google's device flow is restricted to
youtube and youtube.readonly. The loopback flow described here has no such
ceiling, so features that need a broader scope — thumbnail upload, for one —
remain available. Loopback is the more capable choice, not the fallback.
The browser has to be able to reach localhost, which means the tunnel has to
be up and you have to be the one at the keyboard. This is a way to connect
your accounts to your box. It is not a way to let other people sign in to an
installation they do not have shell access to — for that the server needs a real
name, which is the previous section.
One cosmetic wrinkle: if tls.hostname is set while you are browsing through
the tunnel, the setup card warns that you are browsing localhost while the
server is configured as something else. In this flow that warning is expected
and can be ignored.
<dataDir>/
polyemesis.db configuration, destinations, credentials, the library index
secret.key decrypts stored OAuth tokens and client secrets
recordings/ segments, stems/, clips/, exports/
hls/ playout segments
tls/ generated CA and certificates (dir 0700, keys 0600)
Back this up, and treat it as secret material. secret.key is what decrypts
your stored platform tokens; without it they are unrecoverable, and with it
anyone can use them.
Nothing outside this directory is written at runtime, which is also what makes the container's single volume mount sufficient.
Two, both for Rumble chat, and everything else is a file key or a flag. The restraint is deliberate: with three mechanisms it stops being obvious which one won, so a variable has to earn its place.
| Variable | What it does |
|---|---|
RUMBLE_CHAT_API_KEY |
The Rumble chat credential, read by internal/chat/rumble.go. It is not the stream key. It comes from rumble.com/account/livestream-api and lives in the environment because Rumble's chat API has no sign-in, so there is no account to store it against. Treat the URL as a secret — it is the whole credential |
RUMBLE_CHAT_CHANNEL |
Which channel to read, when the key alone does not say |
Both are read at startup by internal/api/chat_wiring.go. See
PLATFORMS.md for the Rumble setup.
For containers, pass flags in command:, or mount a config.yaml. The
docker-compose.yml in the repo shows both.
- INSTALL.md — getting it running on each platform
- QUICKSTART.md — first stream in about five minutes
- TROUBLESHOOTING.md — when it is running but wrong
-
TLS.md — the long version: how
autoresolves, worked configurations for all five deployments, trusting the self-signed CA, and reverse proxies
Getting it running
- Quickstart: from nothing to a live restream
- Install polyemesis — an SRT server on your own box
- OBS SRT setup: multitrack audio to one ingest
- TLS certificates for a self-hosted SRT server
The routing
- Audio routing: a different mix per destination
- Renditions: one shared video encode
- Encoding: what is copied and what is encoded
- Hardware encoding: NVENC, QSV, VA-API, AMF
Operating it
- Configuration: config.yaml and the web UI
- Streaming platforms: what can be automated
- Broadcasting from a file, on a schedule
- What a settings change restarts, and what it does not
- Upgrading polyemesis and its database
- Troubleshooting: SRT, RTMP and audio problems
Automating it
- Monitoring: Prometheus metrics and alerts
- Lifecycle webhooks: one signed POST per event
- MQTT telemetry and Home Assistant
- HTTP API reference — polyemesis /api/v1
Understanding it