Docs

Operations

How to run omlb day to day: validate before you ship, override scalars per environment, understand exactly what a hot reload will and won't touch, run it under systemd, and watch it with the admin API, Prometheus, and the built-in web UI.

Validate in CI

Make --check the gate in your pipeline. It refuses to pass a config whose routes point at upstreams that don't exist, whose addresses don't parse, whose cert files are missing, or whose ACME block doesn't cohere — and because the schema rejects unknown keys, a typo'd setting fails right here instead of being silently ignored in production for six months.

omlb --check -c config.yaml   # non-zero exit on any error — wire this into CI before deploy

Environment variable overrides

The file isn't the last word: OMLB_* environment variables override scalar knobs on top of it (OMLB_LOG_LEVEL, OMLB_THREADS, OMLB_ADMIN_ADDRESS, …). In practice that means one container image and one YAML travel unchanged from staging to production, and only the environment differs.

OMLB_LOG_LEVEL=debug OMLB_THREADS=8 omlb -c config.yaml   # OMLB_* overrides scalar knobs
# layering: built-in defaults -> config file -> OMLB_* env, so one image ships everywhere

Hot reload semantics

Editing the file is the reload — a watcher picks the change up and swaps it in atomically, without dropping a connection. But not everything is swappable, and omlb is strict about the difference: structural changes are refused outright rather than half-applied, and the last good config keeps serving.

on editresult
routes, rewriteslive-swapped
plugin chainslive-swapped
per-route cache settingslive-swapped
per-pool retry / timeoutlive-swapped
listenersrejected — restart required
backendsrejected — restart required
load-balancing algorithmrejected — restart required
health checksrejected — restart required
TLS / SNIrejected — restart required
HA settingsrejected — restart required

Under systemd, systemctl reload omlb additionally calls the admin POST /reload endpoint; use systemctl restart omlb for structural changes.

systemd

Install the binary, a dedicated unprivileged user, and the unit files:

# 1. Binary
sudo install -m0755 target/release/omlb /usr/local/bin/omlb

# 2. Dedicated unprivileged user
sudo useradd --system --no-create-home --shell /usr/sbin/nologin omlb

# 3. Config + plugins
sudo mkdir -p /etc/omlb/plugins
sudo cp config/omlb.yaml /etc/omlb/omlb.yaml

# 4. Unit(s)
sudo cp examples/systemd/omlb.service /etc/systemd/system/       # standalone
sudo cp examples/systemd/omlb@.service /etc/systemd/system/      # HA / multi-instance
sudo systemctl daemon-reload

Standalone:

sudo systemctl enable --now omlb
systemctl status omlb

HA pair — each node runs its own config (ha-node-a.yaml / ha-node-b.yaml as /etc/omlb/<name>.yaml). Enable the matching instance on each node; do not start both on one host:

# On host A only:
sudo systemctl enable --now omlb@ha-node-a

# On host B only:
sudo systemctl enable --now omlb@ha-node-b

The templated unit ships CAP_NET_ADMIN, required for VIP takeover via ip addr add. Without HA you can drop that capability from the unit.

systemd captures stdout into the journal automatically. Set observability.log_format: journald for structured fields — tracing-journald maps each event field to an uppercased F_-prefixed key (F_STATUS, F_BACKEND, F_PATH, F_METHOD, F_UPSTREAM, F_CLIENT_IP, F_DURATION_MS, F_RETRIES, F_ERROR), so you can filter on them directly:

journalctl -u omlb -f                 # follow
journalctl -u omlb@ha-node-a -f       # a specific HA node
journalctl -u omlb -o json-pretty     # inspect all structured fields
journalctl -u omlb TARGET=omlb::access # access log entries only
journalctl -u omlb F_STATUS=502        # only requests that got a 502
journalctl -u omlb PRIORITY=3          # warnings + errors

Admin API, metrics, logs

Three read-side surfaces, each on its own localhost listener. The admin REST API (127.0.0.1:9090) reports status and takes actions: drain or undrain a backend, trigger a reload, load or unload a plugin. Prometheus metrics (127.0.0.1:9091) cover requests and responses by class, upstream errors, ejections, cache hits and misses, and the omlb_abuse_* counters from the security gate. Logs come out pretty, as JSON, or as native journald — whichever your setup drinks.

Web UI

For the times you'd rather look at the box than curl it, there's a built-in dashboard. Enabling it costs no extra deployment — it comes up alongside the proxy on a separate listener — and it reads only the counters and snapshots the proxy already keeps, so having it open costs the data plane nothing.

webui:
  enabled: true
  address: "127.0.0.1:9095"   # bind localhost; tunnel in or front via the proxy (see below)
  capture_logs: false         # opt-in live log tail — small per-request cost when on

What it shows

  • Dashboard — live charts of req/s, response classes, in-flight requests, cache hit rate, and bans enforced, sampled off the hot path at sample_interval.
  • Services — every pool and its backends with ready/ejected/in-flight/weight state, plus a resolution view tracing route → upstream → filters, listeners, and L4 proxies.
  • Logs — a live tail, but only if you set capture_logs: true: it's opt-in because it adds a small per-request cost. Off by default; journalctl has you covered anyway.
  • Config — the effective merged YAML, plus forms for adding a domain or a service.

Reaching it safely

It binds localhost on purpose — this is a thing that can edit your running config. Two sane ways in:

SSH tunnel:

ssh -L 9095:127.0.0.1:9095 host
# then open http://127.0.0.1:9095

Or route it through omlb itself, behind the built-in basic_auth filter — at that point it's just another upstream.

The overlay write boundary

UI edits obey the same rules as hot reload. Adding a domain — a new route onto an upstream that already exists — applies immediately. Adding a service — a new upstream with backends — is structural, so it's staged and waits for a graceful restart. The UI labels each outcome and offers the restart button when one is needed.

Staged and applied edits both persist to a small overlay file merged onto the base config at load, <base>.webui.yaml by default. Base config baked into an image or mounted :ro? Point webui.overlay_path somewhere writable.