If you run a home server, you probably have a rough idea of whether things are working — you can SSH in, run top, check disk space, maybe peek at some logs. But that’s reactive. You only look when something feels wrong. Proper monitoring means you know before something breaks, or at the very least, you know the moment it does.

This is the story of how I set up a full observability stack on my home server, bitfrost — an Ubuntu 24.04 machine running several Docker-based apps.

The Stack

Rather than reinvent anything, I used the same open-source tools that power monitoring at companies running millions of servers:

  • Prometheus — scrapes and stores metrics as time-series data
  • Node Exporter — exposes the host’s hardware metrics (CPU, RAM, disk, network) to Prometheus
  • Grafana — the visualisation layer; dashboards, graphs, the works
  • Loki — log aggregation, think Prometheus but for logs
  • Promtail — runs on the host, tails log files and ships them to Loki
  • Alertmanager — receives alerts from Prometheus and routes them to email, Slack, or wherever you want

Each tool runs in its own Docker container, all wired together via Docker Compose and a shared internal network.

What It Monitors

Out of the box, the stack tracks:

  • CPU usage (per core and overall)
  • Memory — total, used, available, cached
  • Disk space and I/O
  • Network traffic — bytes in/out per interface
  • System load average
  • Running processes
  • All Docker container logs from the host

Beyond that, any application that exposes a /metrics endpoint can be added as a Prometheus scrape target in minutes.

Alerts

The stack comes pre-configured with alert rules for the most common failure scenarios:

Alert Triggers when
InstanceDown Host unreachable for more than 1 minute
HighCPUUsage CPU above 80% for 5 consecutive minutes
LowMemory Available memory drops below 10%
DiskSpaceLow Root filesystem less than 15% free
DiskSpaceCritical Root filesystem less than 5% free

Alertmanager handles routing — uncomment an email or Slack webhook in the config and you’ll get notified wherever you like.

Grafana Dashboards

Once running, the first thing to do in Grafana is import dashboard 1860 — Node Exporter Full. It’s a community dashboard that gives you an immediate, comprehensive view of everything happening on your server without building a single panel yourself.

Datasources (Prometheus and Loki) are provisioned automatically, so there’s no manual wiring needed. You open Grafana, import the dashboard, and you’re looking at real data.

PromQL

One of the most useful things to learn alongside this setup is PromQL — Prometheus Query Language. It’s how Grafana fetches its data, and writing queries directly in the Prometheus UI is a great way to understand what you’re actually measuring.

A few examples to get started:

# CPU usage as a percentage
100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# Available memory as a percentage
(node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

# Disk space used on root filesystem
100 - ((node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100)

Lessons Learned

No setup like this goes completely smoothly. A few things worth noting for anyone doing the same.

Port mapping syntax matters. Docker Compose port mappings are host:container. Grafana always listens on port 3000 inside its container. If you want it on a different host port, it’s 4400:3000 — not 4400:4400. That one colon cost a couple of hours of debugging.

iptables and nftables can conflict. Ubuntu 24.04 uses nftables under the hood, and Docker’s iptables rules sometimes don’t behave as expected when multiple Docker networks are in play. If a container is reachable from inside the host but not from the network, start by checking sudo nft list ruleset.

Keep credentials out of version control. Use a .env file for passwords, add it to .gitignore, and commit a .env.example with placeholder values. Simple discipline that matters.

The Repository

The full setup is on GitHub: homelab-monitoring

Clone it, copy .env.example to .env, set a password, and run docker compose up -d. Everything else is handled automatically.


This is the foundation. Next steps are adding application-level monitoring for the other services running on bitfrost, setting up Slack alerting, and getting deeper into PromQL. But even at this stage, having a real-time view of what the server is doing — and knowing it will tell you when something goes wrong — is a meaningful upgrade over flying blind.