Installation
Yagra is two long-running binaries plus a static WebUI: Yagra-core (orchestration, scheduling,
the northbound REST API), Yagra-poller (stateless ICMP/SNMP/API workers), and Yagra-web
(the WebUI, served by nginx, which reverse-proxies /api to core). This page covers every
supported way to install that stack, from a one-command evaluation box to remote-site pollers
streaming results home over a TLS bus.
Behind them sit up to six backing services:
| Backing service | Role | Needed by |
|---|---|---|
| PostgreSQL | Metadata: nodes, configuration, thresholds, users, alert history | core (required) |
| NATS (JetStream) | The core⇄poller bus: jobs, working sets, results, events | core + poller (required) |
| VictoriaMetrics | Time-series store — the metrics body | core (required) |
| Redis | Ephemeral poller liveness/assignment mirror — rebuildable | core (optional) |
| VictoriaLogs | Passive-event log store — full-text syslog/trap search | core (optional) |
| ClickHouse | Traffic-flow store — NetFlow/IPFIX/sFlow records | core (optional) |
Pollers talk to NATS only — device credentials, job specs, and results all flow over the bus, and a poller never touches the databases. That is what makes pollers stateless, horizontally scalable, and deployable behind a remote site’s NAT.
Choosing a deployment
Section titled “Choosing a deployment”Two axes: single-node vs. distributed polling, and Docker vs. native processes.
| Docker Compose | Native (no Docker) | |
|---|---|---|
| Single node | A — build from source · B — pre-built images | C |
| Distributed pollers | D | E |
New to Yagra? Start with A (one command, builds locally) or B (production-shaped, pulls pre-built images). Reach for D or E once you need pollers at remote sites — they connect out to the central bus, so they cross NAT and firewalls without any inbound port at the site.
Scaling out is a config change, not a rewrite: the single-node and distributed deployments run the same images. You add remote pollers, and — because job messages carry device credentials — you turn on NATS TLS + auth before the bus leaves the machine.
High availability is orthogonal to this matrix: any deployment with more than one core against
the same stores can enable leader election (YAGRA_ENABLE_HA) for automatic core failover.
docker-compose.ha.yml in the repository is a ready-made two-core overlay for trying it:
docker compose -f docker-compose.yml -f docker-compose.ha.yml up --buildExactly one core (the leader) answers 200 on /readyz; the standby answers 503 until it takes
over, within seconds of the leader stopping. Route API and WebUI traffic on /readyz. Details:
high availability.
Images & tags
Section titled “Images & tags”Three images are published to GitHub Container Registry:
ghcr.io/horryworks/yagra-coreghcr.io/horryworks/yagra-pollerghcr.io/horryworks/yagra-webOnly releases are published. Development builds never reach the registry, so every tag below is a release you can run.
| Tag | Meaning | Use it for |
|---|---|---|
:v<version> |
One release (e.g. :v0.1.18) |
Production — pin these |
:latest |
The latest stable release. Pre-releases (-beta, -rc) never move it |
Evaluation, staying current |
:<git-sha> |
Immutable reference to one release | Reproducible deploys, rollback |
The Compose files select the tag via YAGRA_IMAGE_TAG (default latest). Upgrading means
changing the tag and recreating — see Upgrades & backups. Rollback is the
same operation with an older tag.
A — Single node, Docker (build from source)
Section titled “A — Single node, Docker (build from source)”The developer / evaluation box. docker-compose.yml builds the images locally and runs the
whole stack — core, a poller, the WebUI, and all backing stores — on one host:
git clone https://github.com/horryworks/Yagra.gitcd Yagradocker compose up --buildThen open the WebUI at https://localhost:8443 and sign in with the one-time admin password
printed in the core logs. The certificate is self-signed on a first start, so your browser will
warn — see WebUI TLS. The quickstart walks through this path step by
step, including first login.
B — Single node, Docker (pre-built images)
Section titled “B — Single node, Docker (pre-built images)”The production-shaped single-node deployment. docker-compose.deploy.yml pulls images from
GHCR (no local build), is parameterized via .env, and adds a one-shot kek-init service that
writes a persistent key-encryption key so stored monitoring credentials survive redeploys.
-
Clone the repository and create your
.env:Terminal window git clone https://github.com/horryworks/Yagra.gitcd Yagracp .env.example .env -
Edit
.env. The essentials:YAGRA_IMAGE_TAG=v0.1.22 # pin a stable release (see the releases page)POSTGRES_PASSWORD=change-me # change for any non-throwaway boxYAGRA_API_PORT=8080 # host port for the API (plaintext)YAGRA_WEB_PORT=443 # host port for the WebUI (HTTPS)# YAGRA_ADMIN_PASSWORD=choose-a-strong-password # else a one-time random one is logged# YAGRA_PUBLIC_DASHBOARD=false # true = read-only dashboards without loginEverything else has a working default. Full variable list: the configuration reference.
-
Pull and start:
Terminal window docker compose -f docker-compose.deploy.yml pulldocker compose -f docker-compose.deploy.yml up -d -
Retrieve the one-time
adminpassword (unless you setYAGRA_ADMIN_PASSWORD):Terminal window docker compose -f docker-compose.deploy.yml logs core | grep -i passwordOpen the WebUI at https://localhost, sign in as
admin, and change the password. Your browser will warn about the self-signed certificate — see WebUI TLS for how to replace it.
What’s running. Core, one poller, and the WebUI, plus PostgreSQL, Redis, NATS,
VictoriaMetrics, VictoriaLogs (passive-event search), ClickHouse (traffic flows), and an
ipasn-updater sidecar that keeps the offline IP→ASN dataset fresh — the only container with
internet egress. Migrations run automatically on core startup; named volumes preserve all
persistent data across down/up.
| Purpose | Host default | Change via |
|---|---|---|
| WebUI (HTTPS) | 443 |
YAGRA_WEB_PORT |
REST API + /metrics (plaintext) |
8080 |
YAGRA_API_PORT |
| syslog intake (UDP) | 514 |
YAGRA_SYSLOG_PORT |
| SNMP trap intake (UDP) | 162 |
YAGRA_TRAP_PORT |
| NetFlow v5/v9 / IPFIX intake (UDP) | 2055 |
YAGRA_FLOW_PORT |
sFlow intake (UDP; listener off until YAGRA_SFLOW_BIND is set) |
6343 |
YAGRA_SFLOW_PORT |
The stores stay on the internal Docker network. Optional features switch off by setting their
variable empty in .env — e.g. YAGRA_CLICKHOUSE_URL= disables flow monitoring,
YAGRA_SYSLOG_BIND= disables syslog intake. Full matrix:
ports · configuration.
Point your devices at it. Send syslog to the host’s :514/udp, SNMP traps (v1/v2c, informs
included) to :162/udp, and NetFlow/IPFIX export to :2055/udp. Passive events correlate to
nodes by the datagram’s source IP — if Docker’s bridge networking rewrites source addresses on
your host, switch the poller service to network_mode: host so the real address survives.
Verify. All containers up, and core answering:
docker compose -f docker-compose.deploy.yml pscurl -fsS http://localhost:8080/healthzSettings ▸ System Health in the WebUI then shows each backing store’s reachability and the server’s own overall verdict.
Credential persistence — the KEK. Stored monitoring credentials (SNMP communities, SNMPv3
credentials, API tokens) are envelope-encrypted with a master key (KEK). The kek-init service
generates a 32-byte KEK into the kekdata volume once and never overwrites it; core mounts it
read-only. Without a persistent KEK, core falls back to an ephemeral key regenerated on every
restart — and every stored credential becomes undecryptable after a redeploy.
WebUI TLS
Section titled “WebUI TLS”The WebUI is HTTPS out of the box — there is no plain-HTTP listener to publish by accident. On a first start core generates a self-signed certificate covering loopback and the container’s hostname, so your browser will warn, and will usually object to the name as well: nothing inside the container can know the address you will type.
Fix it from Settings ▸ TLS, which is reachable through the warning. You can either import a PEM certificate chain and private key (pasted or from a file), or regenerate the self-signed one with the hostnames and IP addresses you actually use. An imported certificate is live within seconds with nothing restarted, and Yagra refuses a mismatched pair, an expired certificate, or one with no subject alternative name — saying which, rather than failing at the next handshake.
Set YAGRA_WEB_TLS=off in .env when an external reverse proxy or load balancer already
terminates HTTPS in front of the container.
C — Single node, native
Section titled “C — Single node, native”Running the binaries directly, no Docker. You provision the stores yourself, build the workspace,
and run yagra-core + yagra-poller as services (e.g. systemd).
1. Provision the backing stores
Section titled “1. Provision the backing stores”Install and start, reachable from the host that will run core:
-
PostgreSQL 17 — create a database and role (core runs its migrations itself; it does not create the database):
CREATE ROLE yagra LOGIN PASSWORD 'yagra';CREATE DATABASE yagra OWNER yagra; -
NATS 2.x with JetStream —
nats-server -js -
VictoriaMetrics —
victoria-metrics-prod --retentionPeriod=12(12 months of metrics) -
Redis 7 (optional) — enables the poller liveness/assignment mirror; its absence only degrades, never blocks startup
-
VictoriaLogs (optional) — full-text passive-event search; without it, events stay entirely in PostgreSQL
-
ClickHouse 24.x (optional) — the traffic-flow store; without it, flow monitoring is off and the flow API answers 503
2. Build the workspace
Section titled “2. Build the workspace”Requires Rust 1.90 and (for the WebUI) Node 22. Build from the repository root so the workspace’s vendored dependency patches apply:
git clone https://github.com/horryworks/Yagra.gitcd Yagracargo build --release --workspace # → target/release/yagra-core, target/release/yagra-pollercd web && npm ci && npm run build # → web/dist/ (static SPA bundle)3. Provision the KEK (before first core start)
Section titled “3. Provision the KEK (before first core start)”The envelope-encryption master key — a persistent 32-byte file. Without it, core boots with an ephemeral dev key and stored credentials won’t survive a restart:
sudo install -d -m 0700 /etc/yagrahead -c 32 /dev/urandom | sudo tee /etc/yagra/kek > /dev/nullsudo chmod 0400 /etc/yagra/kekBack this file up. Losing it makes every stored credential permanently undecryptable.
4. Run core
Section titled “4. Run core”export YAGRA_DATABASE_URL="postgres://yagra:yagra@localhost:5432/yagra"export YAGRA_BUS_URL="nats://localhost:4222"export YAGRA_TSDB_URL="http://localhost:8428"export YAGRA_REDIS_URL="redis://localhost:6379" # optionalexport YAGRA_LOGS_URL="http://localhost:9428" # optional: passive-event log storeexport YAGRA_CLICKHOUSE_URL="http://localhost:8123" # optional: traffic-flow storeexport YAGRA_KEK_FILE="/etc/yagra/kek"export YAGRA_API_ADDR="0.0.0.0:8080" # default# export YAGRA_ADMIN_PASSWORD="choose-a-strong-password" # else a one-time random one is loggedexport RUST_LOG=info
./target/release/yagra-coreOn startup core connects to the stores, runs its migrations automatically, seeds the built-in
profiles and catalog, and serves /api/v1 + Prometheus /metrics on YAGRA_API_ADDR — check it
with curl -fsS http://localhost:8080/healthz. If YAGRA_ADMIN_PASSWORD was unset, grep the logs
for the one-time admin password. If any of the three required URLs (YAGRA_DATABASE_URL,
YAGRA_BUS_URL, YAGRA_TSDB_URL) is missing, core runs in an in-memory skeleton mode instead of
live. For a permanent installation, wrap this in a service unit (systemd or equivalent) with the
same environment; core is safe to restart at any time. Full variable list: the
configuration reference.
5. Serve the WebUI
Section titled “5. Serve the WebUI”web/dist/ is a static bundle; serve it with any web server and reverse-proxy /api to core.
Mirror the shipped nginx config (web/nginx.conf) — the essentials are that SSE needs
proxy_buffering off and a long proxy_read_timeout, and the SPA needs a try_files fallback:
server { listen 80; root /var/www/yagra; # the contents of web/dist/ index index.html;
location /api/ { proxy_pass http://localhost:8080; # → core's YAGRA_API_ADDR proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_http_version 1.1; proxy_buffering off; # stream SSE immediately proxy_read_timeout 1h; # keep SSE connections open }
location / { try_files $uri $uri/ /index.html; # SPA client-side routing fallback }}6. Run the poller
Section titled “6. Run the poller”The poller needs raw sockets for ICMP. Grant the capability to the binary (so it can run non-root), or run it as root:
sudo setcap cap_net_raw+ep ./target/release/yagra-poller
export YAGRA_BUS_URL="nats://localhost:4222"export YAGRA_POLLER_ID="poller-1" # unique per poller; defaults to hostnameexport YAGRA_POLLER_POOL="default"# Optional intake listeners, each off until bound (binding :514/:162 directly# would additionally need root or CAP_NET_BIND_SERVICE — these high ports don't):# export YAGRA_SYSLOG_BIND="0.0.0.0:1514"# export YAGRA_TRAP_BIND="0.0.0.0:1162"# export YAGRA_FLOW_BIND="0.0.0.0:2055"export RUST_LOG=info
./target/release/yagra-pollerThe poller exposes its own Prometheus /metrics on 0.0.0.0:9100.
D — Distributed pollers, Docker
Section titled “D — Distributed pollers, Docker”Run the full stack centrally (as in B) and add pollers at remote sites. Each remote poller
polls its site’s devices locally and streams results back over the bus. Nodes carry a pool
attribute; core assigns each pool’s nodes across its live pollers by consistent hashing and fails
them over automatically. Concepts and behavior in depth:
distributed polling.
Step 1 — Turn on NATS TLS + auth on the central stack
Section titled “Step 1 — Turn on NATS TLS + auth on the central stack”This is the opt-in block already present (commented) in docker-compose.deploy.yml under the
nats service. All five steps are required:
-
Generate a server cert into
./certs. The SAN must include the exact host/IP each poller will dial:Terminal window mkdir -p certsopenssl req -x509 -newkey rsa:2048 -nodes -days 3650 \-keyout certs/server-key.pem -out certs/server-cert.pem \-subj "/CN=yagra-nats" \-addext "subjectAltName=DNS:nats,DNS:core.example.com,IP:203.0.113.10"The cert is self-signed, so it is its own CA. You hand out
server-cert.pem— the public cert only, never the key — in step 5. -
Set bus passwords in
.env(no defaults, on purpose):YAGRA_NATS_CORE_PASSWORD=a-strong-core-bus-passwordYAGRA_NATS_POLLER_PASSWORD=a-strong-poller-bus-passwordYAGRA_NATS_PORT=4222 # host port to publish the bus onYAGRA_CERT_DIR=./certs -
Load the auth/TLS config. In
docker-compose.deploy.yml, comment outcommand: ["-js"]on thenatsservice and uncomment the block below it — it loadsdocker/nats/nats-server.conf, injects the two passwords, mounts./certs, and publishes the bus port. -
Switch the internal clients to TLS — server-wide TLS leaves no plaintext port, so the co-located core and poller must use
tls://too:# on the core service:YAGRA_BUS_URL: tls://core:${YAGRA_NATS_CORE_PASSWORD}@nats:4222# on the local poller service:YAGRA_BUS_URL: tls://poller:${YAGRA_NATS_POLLER_PASSWORD}@nats:4222# and on BOTH, the pinned server cert plus its volume mount:YAGRA_BUS_CA_FILE: /etc/nats/certs/server-cert.pemvolumes:- ${YAGRA_CERT_DIR:-./certs}:/etc/nats/certs:ro -
Hand
certs/server-cert.pem(public cert only) to each remote poller operator — it becomes theirYAGRA_BUS_CA_FILE.
Bring the central stack back up:
docker compose -f docker-compose.deploy.yml up -dThe NATS config gives the core user full access and the poller user least privilege — publish
results/events/heartbeats, subscribe only to jobs and working-set assignments. Note that the one
poller account is shared: any authenticated poller can read any pool’s assignments, so it is
not a tenant boundary. To scope credentials per poller — so a compromised site can only read its
own pool’s device credentials — enable the optional Auth Callout step described in the same
Compose block. Details: security.
Step 2 — Register the poller in the WebUI
Section titled “Step 2 — Register the poller in the WebUI”Go to Settings ▸ Pollers ▸ “Register poller”. It generates a ready-to-use .env for the
remote host — id, pool, and bus URL. Assign the pool you want this poller to serve.
Step 3 — Run the remote poller
Section titled “Step 3 — Run the remote poller”On the remote-site machine, using docker-compose.poller.yml (runs only a poller):
# put the generated .env next to docker-compose.poller.yml,# and the CA cert from step 1 into ./certsmkdir -p certs && cp /path/to/server-cert.pem certs/
docker compose -f docker-compose.poller.yml up -dThree variables are required — Compose errors out if any is unset — and the generated .env
supplies them:
YAGRA_BUS_URL=tls://poller:a-strong-poller-bus-password@core.example.com:4222YAGRA_POLLER_ID=edge-tokyo-1 # stable, unique per pollerYAGRA_POLLER_POOL=tokyo # the pool this poller servesYAGRA_BUS_CA_FILE=/etc/yagra/certs/server-cert.pemPin the image with YAGRA_IMAGE_TAG here too. Per-scenario extras (intake rate caps, flow
collection, the store-and-forward buffer) are all in the
configuration reference.
docker-compose.poller.yml uses host networking — passive syslog/trap correlation keys on the
datagram source IP (bridge NAT would rewrite it), and raw-socket ICMP wants the host’s interfaces
directly — and grants only NET_RAW. The poller appears on Settings ▸ Pollers within a few
seconds of starting, and core begins assigning that pool’s nodes to it.
A few properties worth knowing:
- Scaling a pool = run more pollers with the same
YAGRA_POLLER_POOL(and distinctYAGRA_POLLER_IDs). Core rebalances the pool across them and fails over on loss. - A pool with zero live pollers falls back to legacy per-job publish, so no nodes go dark during a rollout.
- WAN outages don’t hole your history. The poller keeps polling locally and buffers results
(in memory, spilling to the
pollerbufvolume), then replays them on reconnect — metrics are backfilled at their original timestamps. Alerts are deliberately not backfilled; they resume from “now”. - Flow at the edge: with host networking the poller binds
:2055directly, so point the site’s NetFlow/IPFIX exporters at the poller — flows are aggregated at the edge and streamed to core over the same TLS bus.
E — Distributed pollers, native
Section titled “E — Distributed pollers, native”Same as D, but the remote poller is the native binary instead of a container. The central bus TLS + auth setup (D, step 1) is unchanged.
On the remote host, build (or copy) the yagra-poller binary, drop the CA cert somewhere
readable, and run:
sudo setcap cap_net_raw+ep ./yagra-poller
export YAGRA_BUS_URL="tls://poller:a-strong-poller-bus-password@core.example.com:4222"export YAGRA_POLLER_ID="edge-tokyo-1" # unique per pollerexport YAGRA_POLLER_POOL="tokyo"export YAGRA_BUS_CA_FILE="/etc/yagra/certs/server-cert.pem"# Optional intake listeners (see the privileged-port caveat in D):# export YAGRA_SYSLOG_BIND="0.0.0.0:1514"# export YAGRA_TRAP_BIND="0.0.0.0:1162"# export YAGRA_FLOW_BIND="0.0.0.0:2055"export RUST_LOG=info
./yagra-pollerRun it on the host network (not a private network namespace) so passive-event source-IP correlation and raw ICMP work against the site’s real interfaces. Everything else — pools, registration, failover, buffering — behaves exactly as in D.
Upgrades & backups
Section titled “Upgrades & backups”Upgrades are designed to be low-effort and to never lose or corrupt data. Skim the changelog for the release you are moving to first — behavior changes an operator or API client could notice are always listed there.
For Docker deployments, change the tag and recreate:
# in .env: YAGRA_IMAGE_TAG=v0.2.0 (or override inline as below)YAGRA_IMAGE_TAG=v0.2.0 docker compose -f docker-compose.deploy.yml pullYAGRA_IMAGE_TAG=v0.2.0 docker compose -f docker-compose.deploy.yml up -dWhat makes this safe:
- Migrations are expand-contract and run automatically on core startup. N→N+1 is always supported; there is no manual migration CLI.
- The bus is version-tolerant (N/N-1). A new core works with old pollers during a rollout, so upgrade core first and pollers after — including remote sites, one at a time, in any order.
- Pollers are stateless — replace them freely. A pool briefly without pollers falls back to per-job publish, so no node goes dark mid-rollout.
- Persistent data is preserved. The
pgdata(PostgreSQL),vmdata(VictoriaMetrics), andkekdata(KEK) volumes — or their native equivalents — survive image upgrades untouched.
Before a major upgrade, take a backup:
# PostgreSQL — nodes, configuration, users, alert historydocker compose -f docker-compose.deploy.yml exec postgres \ pg_dump -U yagra yagra > yagra-backup.sql
# The KEK — tiny, and irreplaceabledocker run --rm -v yagra_kekdata:/kek busybox cat /kek/key > kek-backup.keyVictoriaMetrics data lives in the vmdata volume — snapshot it with your usual volume tooling
(or VictoriaMetrics’ own snapshot API). Redis is rebuildable; losing it is non-fatal and it
needs no backup. Rollback = re-run with the previous tag; the immutable :<git-sha> tags exist
for exactly this.
Next steps
Section titled “Next steps”- Every environment variable, default, and clamp — the configuration reference
- Which port carries what, and what to firewall — the port matrix
- How pools, working sets, and failover actually behave — distributed polling
- Running more than one core — high availability
- TLS everywhere, the KEK, and per-poller bus credentials — security
- Structured logs, Prometheus metrics, and distributed tracing — observability