Self-hosting
Run your own relay, control API and ingest gateway from the packages, as systemd units.
Everything the managed service runs, on your own hosts: the relay, the control API, the ingest gateway, and the agent that ships usage. They are plain Linux services, one process each, started by systemd. There are no containers and no proxy in front of the relay.
| Piece | What it does | Comes as |
|---|---|---|
Relay (nearglass-relay) | carries video from publishers to viewers; checks every token | the .deb: binary, systemd unit, sysctl file |
Control API (nearglass-control-api) | streams, keys, tokens, seats, usage, caps, webhooks | one binary |
Ingest gateway (nearglass ingest) | takes RTMP, SRT and WHIP from encoders and publishes to the relay | a command of the nearglass CLI tarball |
Usage agent (nearglass-meter) | ships the relay's session records to the API (and is the relay's agent in a fleet) | one binary, built from the same crate as the API |
Without the control API there are no seats, no waiting room and no usage: see A relay on its own at the end.
What you need
- Linux x86_64 hosts with glibc 2.38 or newer (Ubuntu 24.04, Debian 13). On an older release, build the relay package on that release.
- A name for the relay with a certificate from a CA that browsers trust, and inbound UDP 443 to it (the relay speaks QUIC and WebTransport).
- A name for the API (
api.nearglass.com) behind any TLS terminator, and the gateway's ports (ingest.nearglass.com: RTMP 1935 and SRT 9000 are the usual ones, any ports work). - The packages: the relay
.deb, thenearglassCLI tarball, the control API andnearglass-meterbinaries.
Names below are relay.nearglass.com, api.nearglass.com,
ingest.nearglass.com. Secrets are written sk_....
1. The relay package
sudo dpkg -i nearglass-relay_<version>_amd64.debIt installs, and starts nothing:
| Path | What |
|---|---|
/opt/nearglass-relay/moq-relay-ietf | the relay binary |
/lib/systemd/system/nearglass-relay.service | the unit |
/etc/sysctl.d/90-nearglass-relay.conf | raises the UDP buffer caps to 8 MiB |
/etc/nearglass-relay/scope.example.json | the scope file's format |
The unit runs the relay as a transient system user (DynamicUser=yes) with
ProtectSystem=strict, may bind port 443 and nothing else privileged, restarts
on failure after 2 s, and allows 65536 open files. It starts the relay with:
/opt/nearglass-relay/moq-relay-ietf --bind 0.0.0.0:443 \
--tls-cert /etc/nearglass-relay/cert.pem --tls-key /etc/nearglass-relay/key.pem \
--scope-config /etc/nearglass-relay/scope.json \
--coordinator-file /run/nearglass-relay/coordinator.json \
--max-request-id 1000000 --cache-idle-timeout 30 \
--metrics-addr 127.0.0.1:9090Apply the sysctl file once (sudo sysctl --system). Without it the kernel
caps the relay's UDP send buffer at the stock 212992 bytes and the relay
says so in a warning at start (UDP socket bound ... sndbuf_granted=...).
The relay stops on SIGTERM and ignores SIGINT, so systemctl stop is the
way to stop it, and Ctrl-C in a terminal does nothing.
2. The certificate
Put the CA-issued chain and key for relay.nearglass.com at
/etc/nearglass-relay/cert.pem and key.pem. A self-signed certificate works
for tests only: browsers accept one by its SHA-256 hash and for at most 14
days; set NEARGLASS_API_RELAY_CERT_SHA256 and the API hands the hash to every
player.
# /etc/systemd/system/nearglass-relay.service.d/key.conf
[Service]
LoadCredential=key.pem:/etc/nearglass-relay/key.pem
ExecStart=
ExecStart=/opt/nearglass-relay/moq-relay-ietf --bind 0.0.0.0:443 \
--tls-cert /etc/nearglass-relay/cert.pem --tls-key %d/key.pem \
--scope-config /etc/nearglass-relay/scope.json \
--coordinator-file /run/nearglass-relay/coordinator.json \
--max-request-id 1000000 --cache-idle-timeout 30 \
--metrics-addr 127.0.0.1:9090 \
--admin-addr 127.0.0.1:4480 --admin-key-file %d/relay-admin.key \
--session-log /var/log/nearglass-relay/sessions.jsonl
LoadCredential=relay-admin.key:/etc/nearglass-control-api/relay-admin.key
LogsDirectory=nearglass-relay(This is also where the admin port and the session log of step 4 go.)
3. The control API
Make the keys. Each prints the key once and the hash the environment file takes; the service never sees a key, only hashes.
nearglass-control-api keygen operator # ok_... yours, makes accounts
nearglass-control-api keygen ingest # ik_... the gateway's
nearglass-control-api keygen meter # mk_... nearglass-meter's
openssl rand -hex 32 > relay-admin.key # the relay's admin port, 64 characterskeygen also takes secret (sk_) and publishable (pk_), for a single
account configured by the environment (below).
/etc/nearglass-control-api/control-api.env, mode 600:
NEARGLASS_API_LISTEN=127.0.0.1:8790
NEARGLASS_API_STATE_FILE=/var/lib/nearglass-control-api/state.json
NEARGLASS_API_OPERATOR_KEY_SHA256=<sha256 of the ok_ key>
NEARGLASS_API_INGEST_KEY_SHA256=<sha256 of the ik_ key>
NEARGLASS_API_METER_KEY_SHA256=<sha256 of the mk_ key>
NEARGLASS_API_TENANT_KEY_DIR=/var/lib/nearglass-control-api/tenant-keys
NEARGLASS_API_SCOPE_FILE=/etc/nearglass-relay/scope.json
NEARGLASS_API_RELAYS=https://relay.nearglass.com/
NEARGLASS_API_RELAY_ADMIN_URL=http://127.0.0.1:4480
NEARGLASS_API_RELAY_ADMIN_KEY_FILE=/run/credentials/nearglass-control-api.service/relay-admin.key
NEARGLASS_API_RTMP_URL=rtmp://ingest.nearglass.com:1935/live
NEARGLASS_API_SRT_URL=srt://ingest.nearglass.com:9000
NEARGLASS_API_PUBLIC_URL=https://api.nearglass.com
NEARGLASS_API_TRUST_FORWARDED=1The service refuses to start without a state file, without a secret or
operator key, with an operator key but no tenant key directory and scope
file, or without a relay (NEARGLASS_API_RELAYS, or a fleet file).
Hashes must be 64 hex digits; a plain key there is an error.
| Variable | Default | Meaning |
|---|---|---|
NEARGLASS_API_LISTEN | 127.0.0.1:8790 | where it listens; put TLS in front |
NEARGLASS_API_STATE_FILE | none (required) | the state, one JSON file; one process only, nothing locks it |
NEARGLASS_API_OPERATOR_KEY_SHA256 | none | hashes of the operator keys (accounts) |
NEARGLASS_API_SECRET_KEY_SHA256, NEARGLASS_API_ISSUER_KEY_FILE, NEARGLASS_API_PUBLISHABLE_KEYS | none | one built-in account instead of accounts: its secret keys' hashes, its signing key (P-256 PKCS#8 PEM, from nearglass token keygen), its pk_ keys. It has no spending cap and no webhook |
NEARGLASS_API_INGEST_KEY_SHA256, NEARGLASS_API_METER_KEY_SHA256 | none | hashes of the gateway's and the agent's keys |
NEARGLASS_API_TENANT_KEY_DIR | none | each account's signing key, one file each, mode 600; made by the API |
NEARGLASS_API_SCOPE_FILE, NEARGLASS_API_SCOPE_BASE | none | the relay's scope file, which the API writes on every account change, and an optional file of your own entries it keeps (inline keys) |
NEARGLASS_API_SCOPE_CLOCK_SKEW, NEARGLASS_API_SCOPE_TTL | 2, 10 | seconds, in each account's scope |
NEARGLASS_API_ISSUER, NEARGLASS_API_AUDIENCE | nearglass-token, nearglass-relay | must match the relay's scope |
NEARGLASS_API_RELAYS | none | relay URLs, comma separated; each account's URL is this with its scope as the path |
NEARGLASS_API_RELAY_CERT_SHA256 | none | for a self-signed relay certificate |
NEARGLASS_API_RELAY_ADMIN_URL, NEARGLASS_API_RELAY_ADMIN_KEY_FILE | none | the relay's admin port, to close sessions; set both or neither. Without it a lost seat's sessions run to the end of the watch grace |
NEARGLASS_API_RTMP_URL, NEARGLASS_API_SRT_URL | none | the gateway's addresses, shown when a stream is created |
NEARGLASS_API_PUBLIC_URL | none | this API's public address; seated answers name the seat's play_url under it, which the player needs to heartbeat |
NEARGLASS_API_VIEWER_TTL, NEARGLASS_API_PUBLISHER_TTL, NEARGLASS_API_INGEST_TOKEN_TTL | 120, 3600, 86400 | token lifetimes, seconds |
NEARGLASS_API_WATCH_GRACE | 600 | seconds a viewer's session may outlive its token; keep it equal to the scope's watch_grace_secs |
NEARGLASS_API_NAMESPACE_PREFIX | live | first segment of a stream's namespace |
NEARGLASS_API_TENANT_VIEWER_CAP, NEARGLASS_API_TENANT_MAX_STREAMS, NEARGLASS_API_TENANT_MAX_EGRESS_MBPS | 200, 10, 2000 | limits of a new account |
NEARGLASS_API_DEFAULT_SPEND_CAP, NEARGLASS_API_MAX_SPEND_CAP | 50, 500 | a new account's monthly cap and the most it may set, USD |
NEARGLASS_API_COST_PER_GIB, NEARGLASS_API_COST_PER_VIEWER_HOUR, NEARGLASS_API_FREE_VIEWER_HOURS, NEARGLASS_API_FREE_GIB | see Usage and spending caps | what a month costs, and the free part |
NEARGLASS_API_FLEET_FILE | none | regions and relays |
NEARGLASS_API_OPERATOR_ALERT_URL_FILE, NEARGLASS_API_OPERATOR_ALERT_FORMAT, NEARGLASS_API_OPERATOR_ALERT_SECRET_FILE | none, discord | where operator alerts go (Fleet) |
NEARGLASS_API_HLS_URL, NEARGLASS_API_HLS_ORIGIN | none | the LL-HLS fallback's playlist and packager |
NEARGLASS_API_TRUST_FORWARDED | off | 1: take the client address from X-Forwarded-For (behind your TLS terminator) |
NEARGLASS_API_STATUS_NAME | nearglass | the name on the status page |
Run it as a unit. The state and the account keys live in the unit's state directory; the relay admin key comes in as a credential:
# /etc/systemd/system/nearglass-control-api.service
[Unit]
Description=nearglass control API
After=network-online.target
Wants=network-online.target
[Service]
EnvironmentFile=/etc/nearglass-control-api/control-api.env
ExecStart=/opt/nearglass-control-api/nearglass-control-api serve
Restart=on-failure
RestartSec=2
StateDirectory=nearglass-control-api
StateDirectoryMode=0700
DynamicUser=yes
LoadCredential=relay-admin.key:/etc/nearglass-control-api/relay-admin.key
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
[Install]
WantedBy=multi-user.targetStart the API before the relay: on start it writes the scope file the relay reads. Then:
curl -s http://127.0.0.1:8790/v1/health{"ok":true}Back up the state file, the tenant-keys directory (the accounts' signing
keys, which cannot be remade) and <state file>.usage/ (one file a UTC day).
4. The relay, with the API
Add to the relay's command line: the admin port (loopback only, with the key file, 32 characters at least) so the API can close sessions, and the session log, which is what usage is counted from:
--admin-addr 127.0.0.1:4480 --admin-key-file /etc/nearglass-control-api/relay-admin.key \
--session-log /var/log/nearglass-relay/sessions.jsonlThe scope file is the API's. A scope is the path of the connection URL:
each account is /tn_... (its relay URL is
https://relay.nearglass.com/tn_...). The relay reads the file again for new
sessions, and for a scope already in use after its ttl_secs, so account
changes need no restart. If the file goes missing or invalid, scopes keep
their cached state until it expires and then refuse new sessions. They never
fall open.
The relay refuses to start if the admin port cannot bind. The error names only the address:
ERROR moq_relay_ietf: admin port failed; exiting error=admin port 127.0.0.1:4480Other relay flags worth knowing (all in moq-relay-ietf --help):
| Flag | Default | What |
|---|---|---|
--bind | [::]:443 | listen address (UDP) |
--max-request-id | 100 | the unit sets 1000000; the default runs out after about 50 viewer sessions in one-to-one mode |
--cache-idle-timeout | 30 | seconds a track with no viewers stays cached; 0 never evicts |
--metrics-addr | off | Prometheus metrics at /metrics (Monitoring) |
--internal-peer <IP> | none | a peer relay of yours, repeatable. Peers skip token checks: list only addresses your network authenticates, never one clients can connect from. Public addresses are refused without --internal-peer-allow-public |
--admission | on | a saturated relay refuses new viewers and keeps its own (observe measures only, off is the old behaviour); NEARGLASS_ADMISSION |
--admission-busy | 0.85 | the busy share at which it is saturated |
--admission-retry-ms | 2000 | the retry interval a refusal carries |
--cc | bbr | congestion controller: bbr or cubic |
--setup-rate-per-ip, --setup-rate-global | 200/1000, 1000/2000 | new connections a second, and the burst; 0 is off |
--viewer-send-window-kb | 1024 | how much a slow viewer may hold in the relay |
5. The usage agent
nearglass-meter runs beside each relay and ships its session log to the API
every 60 s, counting each session once even across restarts:
nearglass-meter --log /var/log/nearglass-relay/sessions.jsonl --api http://127.0.0.1:8790/ \
--key-file /etc/nearglass-control-api/meter.key --relay-id relay-1--key-file holds the mk_ key. --relay-id names this relay and is
unique per relay. --every changes the period, --once ships what is
there and exits. Rotate the log by renaming it, never by truncating.
Without it nothing is metered: usage stays at zero, so a spending cap
never triggers and no spend alert is sent. A relay in a fleet
runs the same program with --node.
6. The ingest gateway
The gateway takes encoders and publishes to the relay. With the API, it learns streams and their keys from it and takes a publisher token for each:
echo "ik_..." > /etc/nearglass-control-api/ingest.key && chmod 600 /etc/nearglass-control-api/ingest.key
nearglass ingest --relay https://relay.nearglass.com/ \
--control-api http://127.0.0.1:8790/ --control-api-key-file /etc/nearglass-control-api/ingest.key \
--rtmp 0.0.0.0:1935 --srt 0.0.0.0:9000Startup:
INFO nearglass_ingest::cli: RTMP on 0.0.0.0:1935
INFO nearglass_ingest::srt: SRT on 0.0.0.0:9000 (latency 120 ms on our side)| Flag | Default | What |
|---|---|---|
--relay | none | the relay URL; repeat for a failover list |
--control-api, --control-api-key-file | none | the API and the ik_ key |
--rtmp, --srt | off | listen addresses (SRT is UDP) |
--rtmps, --rtmps-cert, --rtmps-key | off | RTMP over TLS |
--whip, --whip-ip, --whip-cert, --whip-key, --whip-bind-ip | off, 0.0.0.0 | WHIP (HTTPS); --whip-ip is the address encoders reach the gateway at |
--srt-latency-ms | 120 | the gateway's side of the SRT latency; the session uses the larger of the two sides, and 4 times the round trip is the guidance |
--uplink-stale-ms | 1000 | frames older than this are dropped |
--aac-opus-frame-ms | 20 | Opus packet length for audio converted from AAC (10 or 20) |
--tls-root | system roots | CA file for the relay's certificate |
The gateway asks the API for its stream list every 3 s, so a new stream takes keys within about 3 s. It ends a session whose stream was deleted or whose key was rotated. If the API is unreachable for 120 s it drops every key and refuses encoders until the API answers again (it fails closed):
ERROR nearglass_ingest::control_api: control API unreachable for 120 s: every stream key dropped
INFO nearglass_ingest::control_api: control API reachable againA refused encoder logs RTMP publish refused or SRT publish refused with
the key's short id and the reason (UnknownKey, Protocol, Expired,
InUse). A stream with no keyframe for a while logs no keyframe for N s: set the encoder's keyframe interval (1 s).
Without the API, give the gateway a keys file and one publisher token:
nearglass ingest keygen --name cam1 --namespace live/cam --protocols rtmp,srtIt prints the key once (key: ...) and a JSON line to put in the keys file,
{"streams": [ ... ]}. Then Nearglass ingest --relay ... --keys keys.json --token-file publisher.tok --rtmp ....
7. Your first account and stream
Make an account with the operator key. The answer holds its secret key once:
curl -s https://api.nearglass.com/v1/tenants -H "Authorization: Bearer ok_..." -d '{"name":"acme"}'The answer has id (tn_...), secret_key (sk_...), publishable_keys,
relays (with the account's scope as the path), limits, spend_cap_usd
(50 by default) and month. From here it is the
Quickstart, against your own api.nearglass.com.
Run on one host, in this order: a stream is created, Nearglass publish --api ... --secret-key-file ... --stream st_... --file ... --loop connects
(its log shows event=Session(Connected), and POST /v1/play/{id}/admit
with the pk_ key answers 201 with a token and the relay URL.
A relay on its own
You can run the relay with no API: your own signing key, a scope file that
trusts it, and tokens made by nearglass token. Nothing counts seats or usage
and there is no waiting room, so your backend does that
(Mint viewer tokens).
nearglass token keygen --private issuer.key.pem --public issuer.pub.pemIt refuses to overwrite (Error: issuer.key.pem exists; not overwriting).
scope.json, as /etc/nearglass-relay/scope.example.json shows it:
{ "default": { "auth": {
"keys": [{ "kid": "key-1", "alg": "ES256", "pem_file": "/etc/nearglass-relay/issuer.pub.pem" }],
"issuers": ["nearglass-token"], "audiences": ["nearglass-relay"],
"clock_skew_secs": 60, "ttl_secs": 300 } } }Only public keys go on the relay. default covers every path without its
own entry; a path like /excavator-1 is its own scope. Then mint:
nearglass token mint --key issuer.key.pem --role publisher --namespace live/cam --ttl 3600 > publisher.tok
nearglass token mint --key issuer.key.pem --role viewer --namespace live/cam --ttl 120 > viewer.tok
nearglass publish --relay https://relay.nearglass.com/ --namespace live/cam --token-file publisher.tok --file film.ts --loop--role is publisher, viewer or controller (which also needs --turn);
--ttl is in seconds, at most 7 days; --token-id random gives the token
an id, so the relay allows one live session for it and can close it. A
token is accepted up to clock_skew_secs after it expires (60 here, 2 for
accounts), so a 1 s token still publishes. The relay checks the host clock
too: while the kernel says the clock is not synchronized it refuses every
token.
What a bad token looks like is on Troubleshooting.
samples/self-host/run.sh in the repository runs all of this on one host.
What was run, and what was not
Run on one Linux host, from the packages only (the .deb unpacked with
dpkg-deb -x, the CLI tarball, the API and agent binaries), everything on
loopback with a self-signed certificate: the control API with accounts, the
relay with its admin port, session log and metrics, the gateway starting and
listening, an account and a stream created, nearglass publish through the API,
a seat admitted, the session log shipped by nearglass-meter --once, the spend
cap and its alert, the bare relay with nearglass token.