Operate

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.

PieceWhat it doesComes as
Relay (nearglass-relay)carries video from publishers to viewers; checks every tokenthe .deb: binary, systemd unit, sysctl file
Control API (nearglass-control-api)streams, keys, tokens, seats, usage, caps, webhooksone binary
Ingest gateway (nearglass ingest)takes RTMP, SRT and WHIP from encoders and publishes to the relaya 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, the nearglass CLI tarball, the control API and nearglass-meter binaries.

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.deb

It installs, and starts nothing:

PathWhat
/opt/nearglass-relay/moq-relay-ietfthe relay binary
/lib/systemd/system/nearglass-relay.servicethe unit
/etc/sysctl.d/90-nearglass-relay.confraises the UDP buffer caps to 8 MiB
/etc/nearglass-relay/scope.example.jsonthe 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:9090

Apply 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 characters

keygen 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=1

The 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.

VariableDefaultMeaning
NEARGLASS_API_LISTEN127.0.0.1:8790where it listens; put TLS in front
NEARGLASS_API_STATE_FILEnone (required)the state, one JSON file; one process only, nothing locks it
NEARGLASS_API_OPERATOR_KEY_SHA256nonehashes of the operator keys (accounts)
NEARGLASS_API_SECRET_KEY_SHA256, NEARGLASS_API_ISSUER_KEY_FILE, NEARGLASS_API_PUBLISHABLE_KEYSnoneone 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_SHA256nonehashes of the gateway's and the agent's keys
NEARGLASS_API_TENANT_KEY_DIRnoneeach account's signing key, one file each, mode 600; made by the API
NEARGLASS_API_SCOPE_FILE, NEARGLASS_API_SCOPE_BASEnonethe 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_TTL2, 10seconds, in each account's scope
NEARGLASS_API_ISSUER, NEARGLASS_API_AUDIENCEnearglass-token, nearglass-relaymust match the relay's scope
NEARGLASS_API_RELAYSnonerelay URLs, comma separated; each account's URL is this with its scope as the path
NEARGLASS_API_RELAY_CERT_SHA256nonefor a self-signed relay certificate
NEARGLASS_API_RELAY_ADMIN_URL, NEARGLASS_API_RELAY_ADMIN_KEY_FILEnonethe 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_URLnonethe gateway's addresses, shown when a stream is created
NEARGLASS_API_PUBLIC_URLnonethis 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_TTL120, 3600, 86400token lifetimes, seconds
NEARGLASS_API_WATCH_GRACE600seconds a viewer's session may outlive its token; keep it equal to the scope's watch_grace_secs
NEARGLASS_API_NAMESPACE_PREFIXlivefirst segment of a stream's namespace
NEARGLASS_API_TENANT_VIEWER_CAP, NEARGLASS_API_TENANT_MAX_STREAMS, NEARGLASS_API_TENANT_MAX_EGRESS_MBPS200, 10, 2000limits of a new account
NEARGLASS_API_DEFAULT_SPEND_CAP, NEARGLASS_API_MAX_SPEND_CAP50, 500a 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_GIBsee Usage and spending capswhat a month costs, and the free part
NEARGLASS_API_FLEET_FILEnoneregions and relays
NEARGLASS_API_OPERATOR_ALERT_URL_FILE, NEARGLASS_API_OPERATOR_ALERT_FORMAT, NEARGLASS_API_OPERATOR_ALERT_SECRET_FILEnone, discordwhere operator alerts go (Fleet)
NEARGLASS_API_HLS_URL, NEARGLASS_API_HLS_ORIGINnonethe LL-HLS fallback's playlist and packager
NEARGLASS_API_TRUST_FORWARDEDoff1: take the client address from X-Forwarded-For (behind your TLS terminator)
NEARGLASS_API_STATUS_NAMEnearglassthe 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.target

Start 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.jsonl

The 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:4480

Other relay flags worth knowing (all in moq-relay-ietf --help):

FlagDefaultWhat
--bind[::]:443listen address (UDP)
--max-request-id100the unit sets 1000000; the default runs out after about 50 viewer sessions in one-to-one mode
--cache-idle-timeout30seconds a track with no viewers stays cached; 0 never evicts
--metrics-addroffPrometheus metrics at /metrics (Monitoring)
--internal-peer <IP>nonea 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
--admissionona saturated relay refuses new viewers and keeps its own (observe measures only, off is the old behaviour); NEARGLASS_ADMISSION
--admission-busy0.85the busy share at which it is saturated
--admission-retry-ms2000the retry interval a refusal carries
--ccbbrcongestion controller: bbr or cubic
--setup-rate-per-ip, --setup-rate-global200/1000, 1000/2000new connections a second, and the burst; 0 is off
--viewer-send-window-kb1024how 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:9000

Startup:

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)
FlagDefaultWhat
--relaynonethe relay URL; repeat for a failover list
--control-api, --control-api-key-filenonethe API and the ik_ key
--rtmp, --srtofflisten addresses (SRT is UDP)
--rtmps, --rtmps-cert, --rtmps-keyoffRTMP over TLS
--whip, --whip-ip, --whip-cert, --whip-key, --whip-bind-ipoff, 0.0.0.0WHIP (HTTPS); --whip-ip is the address encoders reach the gateway at
--srt-latency-ms120the 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-ms1000frames older than this are dropped
--aac-opus-frame-ms20Opus packet length for audio converted from AAC (10 or 20)
--tls-rootsystem rootsCA 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 again

A 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,srt

It 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.pem

It 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.

On this page