Files
Frank dab81b91a3 feat(observability): make the five Dankclaw observability containers quadlets
prometheus, grafana, loki, alloy and node-exporter ran only from
dankclaw/compose.yaml. Rootless podman does not restart bare containers, so
they did not survive the 2026-09-11 reboot for the amd_iommu=off change and
had to be restored by hand — the same gap that had already cost the fleet
Caddy and Pi-hole, which is why the boot-persistence rule exists.

Each is now a rootless Quadlet unit under dankclaw/observability/quadlet/,
installed and dispatched by deploy_observability in deploy-remote.sh, with
the compose definitions retired so only one owner exists per container name.
Images, mounts, arguments and container names are unchanged, so Caddy's
proxies and Prometheus' own scrape config keep resolving.

Two posture changes, both about the unauthenticated surface:

* The three HTTP UIs published on 0.0.0.0 with no authentication of any kind
  (Prometheus even with the lifecycle endpoint enabled). They now publish on
  127.0.0.1: the browser path is the guarded franksdank.cloud vhost, Caddy
  reaches them by name over dankclaw_default, and the host-side homepage
  status timer probes loopback. Nothing legitimately used the LAN port.
* node-exporter bound :9100 on every interface; it now binds exactly its two
  consumers, loopback (homepage timer) and the LAN address (Prometheus, which
  runs in a container and cannot reach the host's loopback).

alloy gains a reachable health endpoint: its UI listens on the container's
own loopback and alloy 1.x refuses a config block for it, so the compose
service had nothing to probe — the homepage status card for alloy read 'down'
continuously. The unit moves the UI with a command-line flag and publishes
127.0.0.1:12345.

grafana keeps its Authentik OAuth client secret without receiving the whole
.env: systemd expands the value from a [Service] EnvironmentFile, which never
reaches the container.
2026-09-11 15:56:39 -07:00

64 lines
2.5 KiB
INI

# Quadlet unit: alloy (rootless, boot-persistent).
#
# Install path on dankclaw: ~/.config/containers/systemd/alloy.container
# Unit name: alloy.service (container name: alloy)
#
# Log shipper. This replaces the compose service of the same name in
# dankclaw/compose.yaml ("Observability" block).
#
# IMAGE. franksdank/alloy is built locally by deploy-remote.sh
# (build_compose_image) from dankclaw/alloy/Containerfile: upstream
# grafana/alloy plus a static busybox for the healthcheck. Pull=never keeps a
# boot from reaching for that local tag at a registry.
#
# NETWORK. It joins dankclaw_default so its config's loki.write endpoint
# resolves the container name loki:3100.
#
# THE UI PORT EXISTS ON PURPOSE. Alloy's UI listens on the container's own
# 127.0.0.1 by default and cannot be moved from the config file (alloy 1.x
# refuses the ui block), so the compose service had no reachable health
# endpoint at all: the homepage status card for alloy reported "down"
# continuously and there was nothing for a deploy to wait on. The command
# below therefore moves the UI to the container's wildcard address and the
# port is published on host loopback only, which puts alloy in the same class
# as the other three: reachable from the host, by Caddy over the private
# network, and by nothing off-host. Alloy has no authentication, which is
# exactly why it is not published wider.
#
# SOURCES. The host journald socket and the host log/container-storage trees
# are bind-mounted read-only; Alloy tails them and pushes to Loki. The config
# lives at /opt/alloy/config/config.alloy, installed from
# shared/alloy/config.alloy on every deploy.
#
# ORDERING. Wants/After loki.service: without a reachable Loki, Alloy's write
# client fails and its log pipeline backs up.
[Unit]
Description=Alloy log shipper (Dankclaw)
After=network-online.target loki.service
Wants=network-online.target loki.service
[Container]
ContainerName=alloy
Image=localhost/franksdank/alloy:latest
Pull=never
Network=dankclaw_default
Exec=run --server.http.listen-addr=0.0.0.0:12345 /etc/alloy/config.alloy
PublishPort=127.0.0.1:12345:12345
Volume=/opt/alloy/config:/etc/alloy
Volume=/run/systemd/journal/socket:/run/systemd/journal/socket:ro
Volume=/var/log:/var/log:ro
Volume=/var/lib/containers:/var/lib/containers:ro
HealthCmd=/usr/local/bin/busybox wget --spider -q http://localhost:12345/-/healthy
HealthInterval=30s
HealthTimeout=10s
HealthRetries=3
HealthStartPeriod=30s
[Service]
Restart=always
RestartSec=10
[Install]
WantedBy=default.target