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.
73 lines
3.3 KiB
INI
73 lines
3.3 KiB
INI
# Quadlet unit: grafana (rootless, boot-persistent).
|
|
#
|
|
# Install path on dankclaw: ~/.config/containers/systemd/grafana.container
|
|
# Unit name: grafana.service (container name: grafana)
|
|
#
|
|
# Dashboards. This replaces the compose service of the same name in
|
|
# dankclaw/compose.yaml ("Observability" block).
|
|
#
|
|
# PUBLISHING. Bound to 127.0.0.1 for the same reason as prometheus: the browser
|
|
# path is https://grafana.franksdank.cloud (Caddy → Anubis → Authentik outpost →
|
|
# grafana:3000 over dankclaw_default) and the host-side homepage status timer
|
|
# probes 127.0.0.1:3002. Publishing on 0.0.0.0 as the compose service did left
|
|
# an off-host path into Grafana that skips the guard chain entirely.
|
|
#
|
|
# AUTH. Grafana is an OAuth2 relying party against Authentik (OAuth2Provider
|
|
# "grafana", confidential client). The guard chain fronts unauthenticated
|
|
# traffic; the login itself happens through the Authentik button inside
|
|
# Grafana. The client secret is NOT written here: it is expanded by systemd
|
|
# from the root .env (EnvironmentFile in [Service], never in [Container], so
|
|
# the container receives exactly this one secret and not the whole file).
|
|
# Without that .env entry the unit will not start — the same dependency every
|
|
# other rendered-secret deploy has.
|
|
#
|
|
# STATE. /opt/grafana/data holds grafana.db. /opt/grafana/provisioning is
|
|
# mounted read-only and was EMPTY as of 2026-09-11, and grafana.db reported
|
|
# zero datasources and zero dashboards: this Grafana is an empty install whose
|
|
# only configured account is the local admin. Nothing here queries Prometheus
|
|
# or Loki yet.
|
|
|
|
[Unit]
|
|
Description=Grafana dashboards (Dankclaw)
|
|
After=network-online.target prometheus.service
|
|
Wants=network-online.target prometheus.service
|
|
|
|
[Container]
|
|
ContainerName=grafana
|
|
Image=docker.io/grafana/grafana:latest
|
|
Network=dankclaw_default
|
|
User=0:0
|
|
PublishPort=127.0.0.1:3002:3000
|
|
Environment=GF_SERVER_ROOT_URL=https://grafana.franksdank.cloud
|
|
Environment=GF_AUTH_GENERIC_OAUTH_ENABLED=true
|
|
Environment=GF_AUTH_GENERIC_OAUTH_NAME=Authentik
|
|
Environment=GF_AUTH_GENERIC_OAUTH_CLIENT_ID=grafana-21ca190cde75d60f
|
|
Environment=GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=${GRAFANA_OAUTH_CLIENT_SECRET}
|
|
# Global authorize path: this Authentik build resolves the provider by the
|
|
# client_id query parameter — /application/o/<slug>/authorize/ does not exist.
|
|
Environment=GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.franksdank.cloud/application/o/authorize/
|
|
Environment=GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.franksdank.cloud/application/o/token/
|
|
# ?api=1 = introspection-style userinfo: returns custom claims (groups) too.
|
|
Environment=GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.franksdank.cloud/application/o/userinfo/?api=1
|
|
Environment=GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=false
|
|
Environment=GF_AUTH_GENERIC_OAUTH_AUTO_UPSERT=true
|
|
Environment=GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_ORG=true
|
|
Environment=GF_AUTH_GENERIC_OAUTH_ORG_ID=1
|
|
Volume=/opt/grafana/data:/var/lib/grafana
|
|
Volume=/opt/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
HealthCmd=wget --spider -q http://localhost:3000/api/health
|
|
HealthInterval=30s
|
|
HealthTimeout=10s
|
|
HealthRetries=3
|
|
HealthStartPeriod=60s
|
|
|
|
[Service]
|
|
# systemd-side only: supplies ${GRAFANA_OAUTH_CLIENT_SECRET} for expansion
|
|
# above. The container does not receive this file.
|
|
EnvironmentFile=/home/dank/serverAppConfigs/.env
|
|
Restart=always
|
|
RestartSec=10
|
|
|
|
[Install]
|
|
WantedBy=default.target
|