Bei mir laufen mittlerweile vier Raspberry Pis im Dauerbetrieb. Sie tragen einen Reverse Proxy, Pi-hole, ein bisschen DynDNS-Kram und ein paar kleinere Container. Damit ich nicht bei jedem “läuft der eigentlich noch?” erst eine SSH-Session aufmachen muss, sollten alle vier zentral in Grafana auftauchen: CPU, RAM, Netzwerk, und dazu noch, was die einzelnen Docker-Container so treiben.
Die Zielarchitektur stand schnell fest. Auf meinem Server tower laufen bereits VictoriaMetrics und Grafana für mein Energie-Monitoring, da sollten die Pi-Daten einfach mit reinlaufen. Jeder Pi bekommt also einen kleinen Agenten, der lokale Metriken sammelt und per remote_write an VictoriaMetrics schickt.
Blieb nur noch die Frage: mit welchem Agenten? Auf einem Pi hatte ich schon Grafana Alloy im Einsatz, weil es alles in einem Container erledigt: Host-Metriken, Container-Metriken, Versand, alles aus einer Config heraus. Bequem, aber mir kam der Ressourcenverbrauch für das, was so ein Pi eigentlich leisten soll, unverhältnismäßig hoch vor. Die naheliegende Alternative ist die klassische Prometheus-Welt: node_exporter für den Host, cadvisor für die Container und ein schlanker Prometheus im Agent-Modus, der beides nur weiterreicht.
Statt das aus dem Bauch heraus zu entscheiden, habe ich einen echten A/B-Test gefahren: zwei baugleiche Raspberry Pi 4B mit 8 GB RAM, auf dem einen Alloy, auf dem anderen die Drei-Container-Kombination, beide mit identischer Datenabdeckung und demselben Scrape-Intervall. Nach über 76 Stunden Laufzeit stand das Ergebnis fest, und ich habe die Konfiguration danach auf alle vier Pis ausgerollt.
Unten stehen beide Configs komplett, dazu die Zahlen, die am Ende den Ausschlag gegeben haben.
Variante 1: Grafana Alloy #
Alloy bündelt alles in einem Container. Die docker-compose.yml sieht entsprechend übersichtlich aus:
docker-compose.yml #
services:
alloy:
image: grafana/alloy:latest
container_name: alloy
restart: unless-stopped
network_mode: host
pid: host
privileged: true
env_file: .env
volumes:
- ./alloy/config.alloy:/etc/alloy/config.alloy:ro
- ./data/alloy:/var/lib/alloy
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /sys:/sys:ro
- /:/rootfs:ro
- /var/run:/var/run:rw
- /run/containerd/containerd.sock:/run/containerd/containerd.sock
- /var/lib/docker:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
command:
- run
- /etc/alloy/config.alloyDazu eine .env mit der Ziel-URL:
# URL der zentralen VictoriaMetrics-Instanz (inkl. /api/v1/write)
VICTORIAMETRICS_URL=http://192.168.1.40:8428/api/v1/writeDie eigentliche Logik steckt in config.alloy. Alloy nutzt dafür eine eigene, deklarative Konfigurationssprache statt YAML:
config.alloy #
// ── System-Metriken ──────────────────────────────────────────
prometheus.exporter.unix "default" {
procfs_path = "/host/proc"
sysfs_path = "/host/sys"
rootfs_path = "/rootfs"
filesystem {
fs_types_exclude = "^(autofs|binfmt_misc|bpf|cgroup2?|configfs|debugfs|devpts|devtmpfs|tmpfs|fusectl|hugetlbfs|iso9660|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$"
mount_points_exclude = "^/(dev|proc|run/credentials/.+|sys|var/lib/docker/.+)($|/)"
mount_timeout = "5s"
}
netclass {
ignored_devices = "^(veth.*|br-.*|docker.*|[a-f0-9]{15})$"
}
netdev {
device_exclude = "^(veth.*|br-.*|docker.*|[a-f0-9]{15})$"
}
}
prometheus.scrape "unix" {
targets = prometheus.exporter.unix.default.targets
forward_to = [prometheus.remote_write.vm.receiver]
scrape_interval = "15s"
}
// ── Docker Container-Metriken (built-in cAdvisor) ────────────
prometheus.exporter.cadvisor "default" {
docker_host = "unix:///var/run/docker.sock"
docker_only = true
}
discovery.relabel "cadvisor" {
targets = prometheus.exporter.cadvisor.default.targets
rule {
target_label = "instance"
replacement = constants.hostname
}
}
prometheus.scrape "cadvisor" {
targets = discovery.relabel.cadvisor.output
forward_to = [prometheus.remote_write.vm.receiver]
scrape_interval = "15s"
job_name = "cadvisor"
}
// ── Remote Write zu VictoriaMetrics ─────────────────────────
prometheus.remote_write "vm" {
endpoint {
url = env("VICTORIAMETRICS_URL")
}
external_labels = {
instance = constants.hostname,
}
}constants.hostname setzt das instance-Label automatisch auf den echten Hostnamen des jeweiligen Pi. Diese Datei läuft also auf jedem Host unverändert, ganz ohne Anpassung.
Variante 2: node_exporter + cadvisor + Prometheus im Agent-Modus #
Hier braucht es drei Container statt einem, dafür ist jeder für sich genommen deutlich schlanker.
docker-compose.yml #
services:
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
network_mode: host
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
- '--collector.filesystem.mount-points-exclude=^/(dev|proc|run/credentials/.+|sys|var/lib/docker/.+)($$|/)'
- '--collector.filesystem.fs-types-exclude=^(autofs|binfmt_misc|bpf|cgroup2?|configfs|debugfs|devpts|devtmpfs|tmpfs|fusectl|hugetlbfs|iso9660|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$$'
- '--collector.netdev.device-exclude=^(veth.*|br-.*|docker.*|[a-f0-9]{15})$$'
- '--collector.netclass.ignored-devices=^(veth.*|br-.*|docker.*|[a-f0-9]{15})$$'
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
network_mode: host
privileged: true
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
devices:
- /dev/kmsg
command:
- '-housekeeping_interval=15s'
- '-docker_only=true'
- '-disable_root_cgroup_stats=true'
prometheus-agent:
image: prom/prometheus:latest
container_name: prometheus-agent
restart: unless-stopped
network_mode: host
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--enable-feature=agent'Zwei Anpassungen bei cadvisor lohnen sich gegenüber der Standard-Config. Erstens -housekeeping_interval=15s statt der Voreinstellung von 1 Sekunde: Gescraped wird ohnehin nur alle 15 Sekunden, alles darunter ist verschwendete CPU-Zeit fürs Lesen von cgroup-Stats. Zweitens -docker_only=true zusammen mit -disable_root_cgroup_stats=true - sonst erfasst cadvisor zusätzlich sämtliche System-Cgroups und Systemd-Services, die man in so einem Setup gar nicht sehen will.
prometheus.yml #
global:
scrape_interval: 15s
external_labels:
instance: pi-three
scrape_configs:
- job_name: 'integrations/unix'
static_configs:
- targets: ['localhost:9100']
relabel_configs:
- target_label: instance
replacement: pi-three
- job_name: 'cadvisor'
static_configs:
- targets: ['localhost:8080']
relabel_configs:
- target_label: instance
replacement: pi-three
remote_write:
- url: http://192.168.1.40:8428/api/v1/writeDer job_name integrations/unix ist kein Zufall, sondern bewusst an Alloys Namensgebung angeglichen. Sonst zeigen fertige Community-Dashboards wie “Node Exporter Full” plötzlich leere Panels, weil die Job-Variable im Dashboard noch auf den alten Namen zeigt. Der instance-Wert (hier pi-three) ist der einzige Teil, der sich von Pi zu Pi unterscheidet, gleich dreimal in dieser Datei, jeweils von Hand pro Host gesetzt.
Der Vergleich: CPU und RAM #
Damit der Vergleich fair ist, mussten am Ende auf beiden Seiten exakt dieselben Metriken ankommen: keine zusätzlichen Labels, keine fehlenden Collectoren. Erst danach habe ich beide Setups parallel über gut 76 Stunden mitlaufen lassen, gemessen per PromQL direkt gegen VictoriaMetrics.
| Metrik | Alloy | node_exporter + cadvisor + Prometheus-Agent | Verhältnis |
|---|---|---|---|
| CPU Ø | 0,0962 Cores | 0,0436 Cores | Alloy ~2,21× |
| CPU Peak | 0,0999 Cores | 0,0561 Cores | Alloy ~1,78× |
| RAM Ø | 179,0 MiB | 119,6 MiB | Alloy ~1,50× |
| RAM Peak | 189,9 MiB | 142,3 MiB | Alloy ~1,34× |
Der CPU-Unterschied ist am deutlichsten: Alloy braucht im Schnitt gut das Doppelte. Bei RAM ist der Abstand kleiner, aber immer noch spürbar - gut 50 % im Mittel. Und das bei identischer Datenabdeckung, derselben Scrape-Frequenz und ohne dass eine Seite irgendwelche Abkürzungen genommen hat.
Auf einem Raspberry Pi 4, der nebenbei noch andere Container stemmen soll, ist das kein Rundungsfehler. Drei schlanke Container schlagen bei mir also einen bequemen. Ich habe die Alternative danach auf alle vier Pis im Cluster ausgerollt. Einziger Wermutstropfen: Bei drei Containern statt einem muss man auch drei Update-Zyklen im Auge behalten. Für den Ressourcen-Gewinn nehme ich das gerne in Kauf.