If you’ve already got a Raspberry Pi running WireGuard, Pi-hole, or any other homelab service, the next logical step is knowing what it’s actually doing at 3 a.m. — CPU load, memory pressure, disk usage, and thermal throttling all matter on a device with no active cooling and an SD card or USB SSD as its only storage. This guide walks through building a proper metrics stack: node_exporter on the Pi, Prometheus to scrape and store the data, Grafana to visualize it, and Alertmanager to page you before something actually breaks.
Everything here is verified against the current stable releases as of August 2026: node_exporter 1.12.1, Prometheus 3.14.0, and Alertmanager 0.34.0, all of which ship official linux-arm64 binaries that run natively on 64-bit Raspberry Pi OS. If you’re on a Pi 3 or older running the 32-bit OS, swap linux-arm64 for linux-armv7 or linux-armv6 in the download URLs below.
Architecture
The setup has three moving parts:
- node_exporter — runs on every Pi you want to monitor, exposes hardware/OS metrics on port 9100.
- Prometheus — scrapes node_exporter on a schedule and stores the time series. Runs on one host (can be the same Pi if it’s the only one, or a dedicated Pi/VM if you have several nodes).
- Grafana — queries Prometheus and renders dashboards. Typically colocated with Prometheus.
If you only have one Pi, it’s fine to run all three on it — the memory footprint is small (node_exporter and Prometheus together sit well under 100 MB of RSS on a lightly loaded home network). If you have multiple Pis (say, one running Pi-hole and another running WireGuard), install node_exporter on each and point a single central Prometheus at both.
Step 1 — Install node_exporter on each Raspberry Pi
Download the ARM64 binary directly from the official GitHub releases — don’t use apt, the Debian/Raspberry Pi OS repo versions lag behind and sometimes omit collectors you’ll want later.
cd /tmp
curl -LO https://github.com/prometheus/node_exporter/releases/download/v1.12.1/node_exporter-1.12.1.linux-arm64.tar.gz
tar xvfz node_exporter-1.12.1.linux-arm64.tar.gz
sudo mv node_exporter-1.12.1.linux-arm64/node_exporter /usr/local/bin/
rm -rf node_exporter-1.12.1.linux-arm64*
Create a dedicated, unprivileged system user for the exporter — never run it as root:
sudo useradd --no-create-home --shell /usr/sbin/nologin --system node_exporter
sudo chown node_exporter:node_exporter /usr/local/bin/node_exporter
Create the systemd unit:
sudo tee /etc/systemd/system/node_exporter.service > /dev/null <<'EOF'
[Unit]
Description=Prometheus Node Exporter
Wants=network-online.target
After=network-online.target
[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter \
--collector.textfile.directory=/var/lib/node_exporter/textfile_collector
[Install]
WantedBy=multi-user.target
EOF
We're enabling the textfile collector directory now because we'll use it for CPU temperature in a moment. Create the directory and start the service:
sudo mkdir -p /var/lib/node_exporter/textfile_collector
sudo chown node_exporter:node_exporter /var/lib/node_exporter/textfile_collector
sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter
sudo systemctl status node_exporter --no-pager
Verify it's exposing metrics:
curl -s http://localhost:9100/metrics | head -20
Step 2 — CPU temperature (the metric node_exporter doesn't give you cleanly)
This is the one gotcha specific to the Pi. node_exporter's hwmon collector is supposed to expose node_hwmon_temp_celsius from any sensor under /sys/class/hwmon, but on several Raspberry Pi models (particularly the Pi 3 and some Pi 4 kernel/firmware combinations) the SoC thermal zone isn't consistently surfaced through hwmon — this is a long-standing, still-open upstream issue (prometheus/node_exporter#1722). The reliable path is reading /sys/class/thermal/thermal_zone0/temp directly and feeding it to node_exporter's textfile collector, which we already wired up above.
Create a small script:
sudo tee /usr/local/bin/pi-temp-metric.sh > /dev/null <<'EOF'
#!/bin/bash
set -euo pipefail
TEXTFILE_DIR=/var/lib/node_exporter/textfile_collector
TMP_FILE="${TEXTFILE_DIR}/pi_temperature.prom.$$"
TEMP_MILLIDEGREES=$(cat /sys/class/thermal/thermal_zone0/temp)
TEMP_CELSIUS=$(awk "BEGIN {printf \"%.2f\", ${TEMP_MILLIDEGREES}/1000}")
cat > "${TMP_FILE}" <
Run it every minute via a systemd timer instead of cron, so it's consistent with everything else running under systemd:
sudo tee /etc/systemd/system/pi-temp-metric.service > /dev/null <<'EOF'
[Unit]
Description=Write Raspberry Pi CPU temperature for node_exporter textfile collector
[Service]
Type=oneshot
ExecStart=/usr/local/bin/pi-temp-metric.sh
EOF
sudo tee /etc/systemd/system/pi-temp-metric.timer > /dev/null <<'EOF'
[Unit]
Description=Run pi-temp-metric every minute
[Timer]
OnBootSec=30
OnUnitActiveSec=60
AccuracySec=5s
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now pi-temp-metric.timer
Confirm the metric shows up:
curl -s http://localhost:9100/metrics | grep pi_cpu_temperature
Repeat this whole step on every Pi you want temperature data from — each one has its own thermal_zone0.
Step 3 — Install Prometheus
Pick the host that will run Prometheus (this can be the same Pi, or a separate one — I'd lean toward whichever one has the most free storage headroom, since Prometheus's local TSDB writes continuously). Same approach: grab the official ARM64 tarball.
cd /tmp
curl -LO https://github.com/prometheus/prometheus/releases/download/v3.14.0/prometheus-3.14.0.linux-arm64.tar.gz
tar xvfz prometheus-3.14.0.linux-arm64.tar.gz
sudo mv prometheus-3.14.0.linux-arm64/prometheus /usr/local/bin/
sudo mv prometheus-3.14.0.linux-arm64/promtool /usr/local/bin/
sudo mkdir -p /etc/prometheus /var/lib/prometheus
sudo mv prometheus-3.14.0.linux-arm64/consoles /etc/prometheus/
sudo mv prometheus-3.14.0.linux-arm64/console_libraries /etc/prometheus/
rm -rf prometheus-3.14.0.linux-arm64*
sudo useradd --no-create-home --shell /usr/sbin/nologin --system prometheus
sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus
sudo chown prometheus:prometheus /usr/local/bin/prometheus /usr/local/bin/promtool
Write the scrape config. List every Pi running node_exporter under targets:
sudo tee /etc/prometheus/prometheus.yml > /dev/null <<'EOF'
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert.rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ["localhost:9093"]
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "node_exporter"
static_configs:
- targets:
- "raspi5.local:9100"
- "raspi4.local:9100"
EOF
sudo chown prometheus:prometheus /etc/prometheus/prometheus.yml
Replace raspi5.local / raspi4.local with your actual hostnames or IPs — and if you're scraping node_exporter on a different host than Prometheus itself, make sure port 9100 is reachable (check ufw/iptables if you've locked things down).
Create the systemd unit:
sudo tee /etc/systemd/system/prometheus.service > /dev/null <<'EOF'
[Unit]
Description=Prometheus
Wants=network-online.target
After=network-online.target
[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus \
--storage.tsdb.retention.time=30d \
--web.console.templates=/etc/prometheus/consoles \
--web.console.libraries=/etc/prometheus/console_libraries \
--web.listen-address=0.0.0.0:9090
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now prometheus
Check the targets are up at http://<prometheus-host>:9090/targets — both prometheus and every node_exporter target should show UP. Thirty days of retention is plenty for a homelab and won't blow through an SD card; drop it to 7-15d if you're tight on storage.
Step 4 — Install Grafana
Grafana publishes an official APT repository with arm64 packages, so unlike Prometheus/node_exporter it's cleaner to install via apt and let it handle upgrades:
sudo apt-get install -y apt-transport-https wget gnupg
sudo mkdir -p /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key
sudo chmod 644 /etc/apt/keyrings/grafana.asc
echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" | \
sudo tee -a /etc/apt/sources.list.d/grafana.list
sudo apt-get update
sudo apt-get install -y grafana
sudo systemctl enable --now grafana-server
sudo systemctl status grafana-server --no-pager
Grafana listens on port 3000 by default. Open http://<grafana-host>:3000 — default login is admin / admin, and it'll force you to change the password on first login. Do that immediately, especially if the Pi is reachable beyond your LAN.
Step 5 — Connect Grafana to Prometheus and import a dashboard
In Grafana: Connections → Data sources → Add data source → Prometheus. Set the URL to your Prometheus host, e.g. http://localhost:9090 if colocated, or http://raspi5.local:9090 if remote. Click Save & test — you should get a green "Successfully queried the Prometheus API" confirmation.
Rather than building panels from scratch, import the community-maintained Node Exporter Full dashboard (ID 1860 on grafana.com) — it covers CPU, memory, disk I/O, filesystem usage, network throughput, and load average out of the box, and is actively maintained. Go to Dashboards → New → Import, enter 1860, click Load, select your Prometheus data source, and import.
For the Pi-specific CPU temperature metric we exported via the textfile collector, add one more panel manually: new panel, query pi_cpu_temperature_celsius{instance="raspi5.local:9100"}, visualization type "Time series" or "Gauge" with thresholds around 70°C (yellow) and 80°C (red, since the Pi starts throttling around 80-85°C). If you're monitoring multiple Pis, drop the instance filter and let the legend split by instance automatically.
Step 6 — Basic alerting with Alertmanager
A dashboard you never look at doesn't help. Since Alertmanager is what you're probably already using at work, wiring it up here is a good excuse to have something you can actually poke at outside office hours.
cd /tmp
curl -LO https://github.com/prometheus/alertmanager/releases/download/v0.34.0/alertmanager-0.34.0.linux-arm64.tar.gz
tar xvfz alertmanager-0.34.0.linux-arm64.tar.gz
sudo mv alertmanager-0.34.0.linux-arm64/alertmanager /usr/local/bin/
sudo mv alertmanager-0.34.0.linux-arm64/amtool /usr/local/bin/
sudo mkdir -p /etc/alertmanager /var/lib/alertmanager
rm -rf alertmanager-0.34.0.linux-arm64*
sudo useradd --no-create-home --shell /usr/sbin/nologin --system alertmanager
sudo chown -R alertmanager:alertmanager /etc/alertmanager /var/lib/alertmanager
sudo chown alertmanager:alertmanager /usr/local/bin/alertmanager /usr/local/bin/amtool
A minimal config that routes everything to a webhook (swap this for Telegram, Slack, or email — the point here is the plumbing, not the receiver):
sudo tee /etc/alertmanager/alertmanager.yml > /dev/null <<'EOF'
route:
receiver: default
group_by: ['alertname', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receivers:
- name: default
webhook_configs:
- url: "http://localhost:9094/alert"
send_resolved: true
EOF
sudo chown alertmanager:alertmanager /etc/alertmanager/alertmanager.yml
sudo tee /etc/systemd/system/alertmanager.service > /dev/null <<'EOF'
[Unit]
Description=Alertmanager
Wants=network-online.target
After=network-online.target
[Service]
User=alertmanager
Group=alertmanager
Type=simple
ExecStart=/usr/local/bin/alertmanager \
--config.file=/etc/alertmanager/alertmanager.yml \
--storage.path=/var/lib/alertmanager
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now alertmanager
Now define the actual alert rules in Prometheus, referencing the file we already pointed to in prometheus.yml:
sudo tee /etc/prometheus/alert.rules.yml > /dev/null <<'EOF'
groups:
- name: raspberry-pi-homelab
rules:
- alert: NodeExporterDown
expr: up{job="node_exporter"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "node_exporter down on {{ $labels.instance }}"
- alert: HighCPUTemperature
expr: pi_cpu_temperature_celsius > 75
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} CPU at {{ $value }}°C"
- alert: LowDiskSpace
expr: |
(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}
/ node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) < 0.10
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} filesystem {{ $labels.mountpoint }} below 10% free"
- alert: HighMemoryUsage
expr: |
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > 0.90
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} memory usage above 90%"
EOF
sudo chown prometheus:prometheus /etc/prometheus/alert.rules.yml
Validate the rule file before reloading — promtool will catch syntax errors that would otherwise silently break rule evaluation:
promtool check rules /etc/prometheus/alert.rules.yml
sudo systemctl reload prometheus
Check http://<prometheus-host>:9090/alerts to confirm the rules loaded, and http://<prometheus-host>:9093 for the Alertmanager UI. From here, swapping the webhook receiver for a Telegram bot or an ntfy topic is a config-only change — no need to touch the rule definitions.
Wrapping up
At this point you've got a self-hosted monitoring stack that covers the basics any SRE would expect: metrics collection (node_exporter), storage and alert evaluation (Prometheus), visualization (Grafana with the Node Exporter Full dashboard), and notification routing (Alertmanager). It's a small footprint — comfortably under 200MB combined RSS in a homelab-sized deployment — and it scales cleanly if you add more Pis or other Linux boxes later: just add a target to prometheus.yml and reload.
A few things worth doing next if you want to take this further: put Prometheus and Grafana behind the same WireGuard tunnel you're already running so you're not exposing 3000/9090/9093 to your LAN unauthenticated, and consider Grafana's built-in alerting or unified alerting if you'd rather manage alert rules from the Grafana UI instead of YAML — though keeping rules in Prometheus, as done here, is the more portable and version-controllable option.