Architecture¶
One central stack, one reusable collector, and a push relationship between them.
Central stack¶
The central node runs four services. It exists in two deployment shapes that must not be confused:
Installed by scripts/lxc-install.sh central. Native systemd services,
with nginx terminating Basic Auth in front of the ingest paths. This is
what is actually deployed.
| Service | Bind | Purpose |
|---|---|---|
| Grafana | 127.0.0.1:3000 |
dashboards, unified alerting |
| Prometheus | 127.0.0.1:9090 |
metrics, remote-write receiver |
| Loki | 127.0.0.1:3100 |
logs |
| Alloy | 127.0.0.1:12345 |
the central node's own telemetry |
| nginx | :80 |
reverse proxy and Basic Auth |
| matrix-webhook | 127.0.0.1:4785 |
Grafana to Matrix relay |
docker-compose.yml runs Grafana, Prometheus, Loki and Alloy as
containers with external volumes. Useful for a fresh deployment
elsewhere; not what this fleet runs.
Telling them apart
Both /prometheus/-/ready and /loki/ready returning 401 means the
direct-LXC path, because only lxc-install.sh writes those auth_basic
blocks.
Collector flow¶
flowchart LR
subgraph host["Any Linux host"]
A["Alloy"]
U["prometheus.exporter.unix<br/>host metrics + textfile"]
C["prometheus.exporter.cadvisor<br/>container metrics"]
J["loki.source.journal<br/>systemd journal"]
D["loki.source.docker<br/>container logs"]
T["/var/lib/node_exporter/<br/>textfile_collector/*.prom"]
U --> A
C --> A
J --> A
D --> A
T -.read by.-> U
end
A -->|"remote_write<br/>HTTPS + Basic Auth"| P["Prometheus"]
A -->|"push<br/>HTTPS + Basic Auth"| L["Loki"]
P --> G["Grafana"]
L --> G
G -->|"webhook"| M["matrix-webhook"]
M -->|"m.room.message"| X["Matrix room"]
Every host, on the tailnet or on the private LAN, uses the same public HTTPS ingest endpoint. That gives exactly one collector config shape in the fleet.
The tradeoff that buys
Internal telemetry makes a round trip through Cloudflare, and guests cannot push while the WAN is down. Accepted deliberately: one config shape is worth more here than a few milliseconds and a rare failure mode. The exception is the central node itself, which pushes over loopback so its own telemetry never depends on the WAN.
Persistence¶
External Docker volumes survive docker compose down -v:
| Volume | Contents |
|---|---|
monitoring-grafana-data |
users, sessions, alert rule state |
monitoring-prometheus-data |
TSDB blocks and WAL |
monitoring-loki-data |
chunks, indexes, compactor state |
monitoring-alloy-data |
WAL and journal positions |
On the direct-LXC deployment the equivalents are /var/lib/grafana,
/var/lib/prometheus, /var/lib/loki and /var/lib/alloy.
Retention is covered in Operations → Retention.
Where config lives¶
Three copies of the Alloy config exist. This is deliberate, and the scoping matters:
| File | Scope |
|---|---|
ansible/roles/alloy_collector/templates/config.alloy.j2 |
authoritative, every native install |
alloy/config.alloy |
Docker collector only |
scripts/lxc-install.sh heredoc |
bootstrap only, guarded by a marker |
Never set these on a native install
rootfs_path, procfs_path and sysfs_path exist because the Docker
collector runs with bind mounts. A native install that sets them reports
the wrong filesystem.
/etc/alloy/.ansible-managed is the marker. write_alloy_config() in
lxc-install.sh returns early when it exists, so lxc-update.sh can no
longer revert Ansible's work. Delete it only to hand ownership back.