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 edit | result |
|---|---|
| routes, rewrites | live-swapped |
| plugin chains | live-swapped |
| per-route cache settings | live-swapped |
| per-pool retry / timeout | live-swapped |
| listeners | rejected — restart required |
| backends | rejected — restart required |
| load-balancing algorithm | rejected — restart required |
| health checks | rejected — restart required |
| TLS / SNI | rejected — restart required |
| HA settings | rejected — 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;journalctlhas 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.