Skip to content
Bytearium

No ads or affiliate links active

Homelab & storage

Homelab monitoring with Prometheus and Grafana

Homelab monitoring with Prometheus and Grafana: a Compose stack with verified image tags, a storage cap, mail alerts and how it differs from Uptime Kuma.

Uptime Kuma wakes you at 3 a.m. to say the server is down. It cannot tell you that the disk has been filling a little faster every week. A graph can, and it does so on a Tuesday afternoon, not at 3 a.m.

The short answer

How do you do homelab monitoring with Prometheus and Grafana? You run four containers with Docker Compose. Prometheus fetches numbers from your machines at a fixed interval and stores them. node_exporter makes CPU, memory, disks and network of a Linux host readable for it. Grafana draws the history, and Alertmanager sends a message when a rule fires [1][4][11][12][16]. Prometheus pulls: it asks every target for its current numbers, by default once a minute [4]. This guide pins image tags that exist (checked 25 Oct 2026), keeps every service on the loopback address, caps how much disk the history may use, and ships two alert rules: a service stopped answering, and a disk will be full within 24 hours. Our assessment: Prometheus answers "how is it trending?", Uptime Kuma answers "is it reachable?". Run both, because they fail differently.

How it works

Four roles. An exporter is a small program that shows the numbers of one system at a web address, normally /metrics. node_exporter does this for Linux hosts and listens on port 9100 by default [12]. Prometheus scrapes (fetches) each configured target, keeps the samples in a time series database on local disk, and records a series up with 1 or 0 for every scrape [4][9]. Grafana stores no metrics itself. It queries Prometheus through a data source and draws the result [28][29]. Alertmanager sends the notifications. The Prometheus docs say that alerting rules only work out what is broken right now and that another layer is needed for summarising, rate limiting and silencing. In the Prometheus world, that layer is Alertmanager [6].

Pinned versions. Prometheus 3.15.1 (released 21 Oct 2026) [18], node_exporter 1.12.1 (14 Jul 2026) [19], Alertmanager 0.34.1 (17 Sep 2026) [20] and Grafana 13.2.4 (13 Oct 2026) [21]. Each tag exists on Docker Hub as of 25 Oct 2026 [22][23][24][25]. We do not use latest, so an update is a decision you make and not something that happens at the next pull.

Where the stack runs. The Docker images are listed for 64-bit ARM and 32-bit ARM (v7) as well as x86-64 [22][23][24][25]. Beyond that, the choice is about your situation:

OptionFits whenWatch out for
Mini PC or old PC with LinuxIt runs around the clock anywayMeasure its power draw first; see the power guides below
Raspberry PiYou want the quietest, smallest boxPrometheus writes to disk all the time: a write-ahead log plus a new block every two hours [1]. Our assessment: use an SSD, not an SD card. We have no wear figures, so that part is unproven
On the monitored host itselfYou want the quickest startIf that host dies, the monitoring dies with it and stays silent
VPSYou want a view from outsidePrometheus pulls, so your home exporters would have to be reachable from the internet. The project says its endpoints should not be exposed to public networks [10]

Our assessment: put the stack on a small always-on machine at home, and put the outside view (Uptime Kuma) on a different device or a VPS. If you really want Prometheus on a VPS, connect it to your home network through a VPN rather than opening exporter ports; see the WireGuard and Tailscale guide below.

Why this does not repeat Uptime Kuma. Uptime Kuma asks from outside whether something answers. Prometheus reads numbers from inside and keeps them:

Uptime KumaPrometheus with node_exporter
Typical questionIs the page, port or host reachable?How full is the disk, and how fast is it filling?
Source of dataChecks it runs itselfExporters on the systems
What you keepUp and down history, response timesAny number the exporters offer, for as long as you allow
Typical alarm"Down for 2 minutes""Will be full in 24 hours"

The two connect: Uptime Kuma serves its monitors as Prometheus metrics at /metrics, with basic authentication by default, so Prometheus can scrape them and Grafana can show availability next to load [36].

Limit the history. Without a limit, the default retention is 15 days [1][3]. Set time and size. Both apply, and whichever triggers first wins [1]. Since the 2025 change in Prometheus, the two command-line flags are marked deprecated and the same settings live in prometheus.yml under storage.tsdb.retention, which takes precedence over the flags [3][5]. Many tutorials still use the flags [37]. The size limit should be at most 80 to 85 percent of the disk you allocate, because compaction needs room for old and new blocks at the same time [1].

Close the doors. Prometheus listens on 0.0.0.0:9090 by default [3]. Its security model assumes untrusted users may read everything it serves, and says the endpoints should not be exposed to public networks [10]. Docker adds a trap: ports you publish with -p are diverted before ufw sees them [34], and a plain -p 9090:9090 is open to everything that can reach the host [33]. We avoid both by running the containers in the host network and binding each service to 127.0.0.1 (Linux only; we did not test Docker Desktop). The node_exporter README itself asks for host network and host PID for host monitoring [12], and Docker creates no firewall rules for host networking, so your own firewall still applies [34].

Setting it up

Everything lives in one folder. Create it and the files below:

monitoring/
├── compose.yaml
├── .env                 # GRAFANA_ADMIN_PASSWORD=choose-a-long-one  (never commit this)
├── prometheus/
│   ├── prometheus.yml
│   └── rules.yml
├── alertmanager/
│   ├── alertmanager.yml
│   └── smtp_password    # your mail password, nothing else in the file
└── grafana/provisioning/datasources/prometheus.yml

compose.yaml. If you pass command: to the Prometheus or Alertmanager image, you replace the image defaults, so the config and storage flags are listed again [2][17]:

name: monitoring

services:
  prometheus:
    image: prom/prometheus:v3.15.1
    restart: unless-stopped
    network_mode: host
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --web.listen-address=127.0.0.1:9090
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./prometheus/rules.yml:/etc/prometheus/rules.yml:ro
      - prometheus-data:/prometheus

  node-exporter:
    image: prom/node-exporter:v1.12.1
    restart: unless-stopped
    network_mode: host
    pid: host
    command:
      - --path.rootfs=/host
      - --web.listen-address=127.0.0.1:9100
    volumes:
      - /:/host:ro,rslave

  alertmanager:
    image: prom/alertmanager:v0.34.1
    restart: unless-stopped
    network_mode: host
    command:
      - --config.file=/etc/alertmanager/alertmanager.yml
      - --storage.path=/alertmanager
      - --web.listen-address=127.0.0.1:9093
      - --cluster.listen-address=
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
      - ./alertmanager/smtp_password:/etc/alertmanager/smtp_password:ro
      - alertmanager-data:/alertmanager

  grafana:
    image: grafana/grafana:13.2.4
    restart: unless-stopped
    network_mode: host
    environment:
      GF_SERVER_HTTP_ADDR: 127.0.0.1
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?set GRAFANA_ADMIN_PASSWORD in .env}
    volumes:
      - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
      - grafana-data:/var/lib/grafana

volumes:
  prometheus-data:
  alertmanager-data:
  grafana-data:

Why these lines matter:

  • --path.rootfs=/host together with the bind mount /:/host:ro,rslave is the node_exporter README recipe. Without them the exporter measures its own container [12]. The prefix is removed from the mountpoint label, so your root disk shows up as / [13].
  • --cluster.listen-address= (empty) switches off Alertmanager's clustering. It is on by default and listens on 0.0.0.0:9094 [16], which in host networking would open a port to your whole LAN.
  • --web.listen-address is the flag for the bind address on Prometheus and Alertmanager [3][38]. node_exporter shares it through the Prometheus exporter toolkit, which also offers --web.config.file for TLS and passwords if you ever need to scrape across the LAN [14]. Check yours with docker run --rm prom/node-exporter:v1.12.1 --help.
  • GF_SERVER_HTTP_ADDR is the environment form of http_addr in the [server] section [27]. The admin password is only read on first start [27], and :? makes Compose stop if .env lacks it [35]. Docker secrets via GF_SECURITY_ADMIN_PASSWORD__FILE are the alternative [26].
  • The files in smtp_password and the configs are read by the container user nobody [17], so they must be readable for it.

prometheus.yml. The default scrape interval is one minute [4]. We use 15 seconds, which is fine for a handful of hosts:

global:
  scrape_interval: 15s
  evaluation_interval: 1m

storage:
  tsdb:
    retention:
      time: 30d
      size: 4GB

rule_files:
  - /etc/prometheus/rules.yml

alerting:
  alertmanagers:
    - static_configs:
        - targets: ["127.0.0.1:9093"]

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["127.0.0.1:9090"]

  - job_name: node
    static_configs:
      - targets: ["127.0.0.1:9100"]

Keep the job name node: the Node Exporter Full dashboard expects exactly that [32]. To watch more machines, install node_exporter on them and add their address:9100 to the node targets. Then bind the exporter to that machine's LAN address and let only the monitoring host reach it. Do not expose it more widely [10].

rules.yml. Two alarms are enough to start. The first comes straight from the Prometheus documentation, the second looks ahead [6][8]:

groups:
  - name: homelab
    rules:
      - alert: InstanceDown
        expr: up == 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.job }} on {{ $labels.instance }} has not answered for 5 minutes"

      - alert: DiskFullWithin24h
        expr: predict_linear(node_filesystem_avail_bytes{job="node", fstype!~"tmpfs|overlay|squashfs"}[6h], 24 * 3600) < 0
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.mountpoint }} on {{ $labels.instance }} will be full within 24 hours at the current rate"

for makes Prometheus wait before an alert counts as firing, which keeps a single missed scrape from waking you [6]. predict_linear fits a straight line through the last six hours and reads it 24 hours ahead. It works on gauges only and needs at least two samples [8]. Our assessment: a big one-off write, such as a backup that lands on the disk, can bend that line, so the 30 minutes of for is a compromise you may tune.

alertmanager.yml. The values below are placeholders. Replace the host, the addresses and the user with those of your mail provider. Alertmanager can read the SMTP password from a file, and it requires TLS to the server unless you change that [15]:

global:
  smtp_smarthost: "smtp.example.org:587"
  smtp_from: "alerts@example.org"
  smtp_auth_username: "alerts@example.org"
  smtp_auth_password_file: /etc/alertmanager/smtp_password

route:
  receiver: mail
  group_by: ["alertname", "instance"]

receivers:
  - name: mail
    email_configs:
      - to: "you@example.org"
        send_resolved: true

Pick a channel that does not depend on the homelab itself. A mail account at an outside provider qualifies. A mail server running on the machine that just crashed does not.

Grafana data source. Grafana reads provisioning files at start [28]. Put the following in grafana/provisioning/datasources/prometheus.yml. A provisioned data source cannot be edited in the web interface afterwards [29]:

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://127.0.0.1:9090
    isDefault: true
    editable: false

Start and check. Run docker compose up -d in the folder. Then:

  • ss -tlnp should show ports 9090, 9100, 9093 and 3000 on 127.0.0.1 only.
  • curl -s http://127.0.0.1:9090/-/healthy returns the health check of Prometheus [39]. Grafana's own check is GET /api/health [30].
  • Open Prometheus on http://127.0.0.1:9090, run up, and expect two series with value 1.
  • For a rule or config change, validate it with docker compose exec prometheus promtool check rules /etc/prometheus/rules.yml and promtool check config /etc/prometheus/prometheus.yml [7], then reload with docker compose kill -s SIGHUP prometheus. A reload is also possible by HTTP, but only with --web.enable-lifecycle, which is off by default and which we leave off [3][10].

From another computer, use an SSH tunnel: ssh -L 3000:127.0.0.1:3000 user@monitoring-host, then open http://127.0.0.1:3000. If you want Grafana in the LAN directly, set GF_SERVER_HTTP_ADDR to the host's LAN address rather than 0.0.0.0, and do not forward any port from your router. For a proper address with HTTPS, put a reverse proxy in front; see the guide below.

Grafana dashboard. Log in as admin with the password from .env. The default is admin/admin if you skipped that step [27], so do not skip it. The quickest dashboard is the community one: Dashboards, New, Import, ID 1860 ("Node Exporter Full"), choose the data source [31][32]. Its page recommends the extra node_exporter options --collector.systemd and --collector.processes for some panels [32], which we did not enable, so a few panels may stay empty. For your own first dashboard, these queries are enough:

PanelQuerySource
CPU in system moderate(node_cpu_seconds_total{mode="system"}[1m])Prometheus node_exporter guide [11]
Free disk spacenode_filesystem_avail_bytes[11]
Network receivedrate(node_network_receive_bytes_total[1m])[11]
Memory availablenode_memory_MemAvailable_bytes / node_memory_MemTotal_bytesmetric name from tutorial [37], check it in your own Prometheus

In practice

How big will the history get? The Prometheus docs give a rough formula: disk space = retention in seconds × samples per second × bytes per sample, with 1 to 2 bytes per sample on average [1]. Your own numbers come from scrape_samples_scraped{job="node"}, which reports how many samples one scrape returned [9]. Here is a worked example with an invented value of 1,500 samples per scrape:

15 s interval        →  1,500 / 15 = 100 samples per second
30 days              →  30 × 86,400 s = 2,592,000 s
samples in 30 days   →  100 × 2,592,000 = 259,200,000
at 1 byte  per sample →  about 0.26 GB
at 2 bytes per sample →  about 0.52 GB

The 1,500 is made up for the method; measure your own. We allocate 5 GB for the Prometheus volume and set size: 4GB, which is 80 percent and follows the 80 to 85 percent advice [1]. The size limit counts the write-ahead log, but only persisted blocks are deleted to honour it [1]. For the test of the alarm, stop the exporter with docker compose stop node-exporter. After five minutes plus one evaluation interval, InstanceDown should fire, and a mail should arrive. Then start it again and wait for the "resolved" mail.

Checklist

  • Check that every image tag exists on Docker Hub before you copy it, and write down the date.
  • Set GRAFANA_ADMIN_PASSWORD in .env before the first start, and keep .env out of git.
  • Bind every service to 127.0.0.1, and confirm it with ss -tlnp.
  • Disable Alertmanager clustering when you run it in the host network.
  • Set retention time and size in prometheus.yml, and keep the size below 85 percent of the allocated disk.
  • Measure scrape_samples_scraped before you trust any size estimate.
  • Run promtool check config and promtool check rules before every reload.
  • Provoke one alarm on purpose, and confirm that the mail arrives outside your homelab.
  • Monitor Grafana and the host from a second device with Uptime Kuma, so the watcher is watched.
  • Decide whether the metric history deserves a backup. The Prometheus docs recommend snapshots, which need the admin API [1][10]. We leave it off, and we do back up the compose folder and the Grafana volume.

Related pages: Uptime Kuma: know when your homelab is down; Prometheus exporter; Containers and OCI; Measuring homelab power correctly; Home server or VPS: the electricity maths; Reverse proxy for a homelab; WireGuard or Tailscale for reaching your homelab; Raspberry Pi 5 storage: SD, USB SSD or NVMe; Homelab backups with restic or Borg.

Primary sources

These sources are the basis of this page. That does not mean every claim was checked individually; whether a review record exists is shown above under “Status”. The interpretation and checklist are our own work.

There is no documented review record for this version yet. The text relies on the sources listed.

How this page was made: Bytearium creates these pages with the help of AI and is responsible for publishing them. AI can be wrong, so we show our sources and review status openly.

  1. Prometheus projectStorage
  2. Prometheus projectInstallation
  3. Prometheus projectprometheus command-line flags
  4. Prometheus projectConfiguration
  5. Prometheus projecttsdb: Deprecate retention flags; add tsdb.retention runtime configuration
  6. Prometheus projectAlerting rules
  7. Prometheus projectDefining recording rules
  8. Prometheus projectQuery functions
  9. Prometheus projectJobs and instances
  10. Prometheus projectSecurity model
  11. Prometheus projectMonitoring Linux host metrics with the Node Exporter
  12. Prometheus projectnode_exporter
  13. Prometheus projectstrip rootfs prefix for run in docker
  14. Prometheus projectWeb configuration
  15. Prometheus projectAlertmanager configuration
  16. Prometheus projectAlertmanager README
  17. Prometheus projectAlertmanager Dockerfile
  18. Prometheus projectReleases · prometheus/prometheus
  19. Prometheus projectReleases · prometheus/node_exporter
  20. Prometheus projectReleases · prometheus/alertmanager
  21. Grafana LabsReleases · grafana/grafana
  22. Docker Hubprom/prometheus tag v3.15.1
  23. Docker Hubprom/node-exporter tag v1.12.1
  24. Docker Hubprom/alertmanager tag v0.34.1
  25. Docker Hubgrafana/grafana tag 13.2.4
  26. Grafana LabsConfigure a Grafana Docker image
  27. Grafana LabsConfigure Grafana
  28. Grafana LabsProvision Grafana
  29. Grafana LabsConfigure the Prometheus data source
  30. Grafana LabsOther HTTP API
  31. Grafana LabsMonitoring a Linux host with Prometheus, Node Exporter, and Docker Compose
  32. Grafana LabsNode Exporter Full
  33. Docker Inc.Port publishing and mapping
  34. Docker Inc.Packet filtering and firewalls
  35. Docker Inc.Interpolation in Compose files
  36. Uptime Kuma projectPrometheus Integration
  37. RootwerkstattGrafana + Prometheus: Homelab-Monitoring, das funktioniert
  38. NSRCSecure your Prometheus install
  39. Prometheus projectManagement API

This page has no ads and no affiliate links.