My Homelab Monitoring Stack: Observability on a Budget
My homelab runs 14 services — reverse proxy, Git server, media stack, backup daemon, a few experiments — on a used mini-PC with 16 GB of RAM and a 1 TB SSD. For a year I ran it with no monitoring at all: services either worked or didn't, and I found out when something stopped responding. Then I spent a weekend building a real observability stack, and the difference isn't just "I get alerts" — it's that I now know what my machines are doing before they break. This is that stack: the pieces, the configs, and the honest lessons from a year of running it.
The stack
Four components, all free, all self-hosted, all running as containers on the same box:
- Prometheus — scrapes metrics from everything, stores time series.
- node_exporter — exposes OS metrics (CPU, memory, disk, network).
- Grafana — dashboards over Prometheus data.
- Loki + Promtail — logs, collected and searchable without a heavy Elastic stack.
- Alertmanager — routes Prometheus alerts to your phone (I use ntfy.sh; no account needed, push notifications for free).
The design goal: metrics answer "is it healthy now?", logs answer "what happened then?", and alerts answer "should I be awake?" — three questions, three tools, one compose file.
The compose file
Everything lives in one docker-compose.yml, because the homelab virtue is explicable:
services: prometheus: image: prom/prometheus:latest volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro - prom-data:/prometheus ports: ["9090:9090"] restart: unless-stopped
node-exporter: image: prom/node-exporter:latest network_mode: host restart: unless-stopped
grafana: image: grafana/grafana:latest environment: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD} volumes: - grafana-data:/var/lib/grafana ports: ["3000:3000"] restart: unless-stopped
loki: image: grafana/loki:latest ports: ["3100:3100"] restart: unless-stopped
volumes: prom-data: grafana-data:The one-line secrets note: ${GRAFANA_PASSWORD} comes from a .env file next to the compose file — never commit credentials, even for a homelab. The whole stack idles around 1.5 GB of RAM, which is the entire budget allocation and the reason I chose Loki over Elasticsearch.
What to scrape: the 80/20
You don't need to instrument everything on day one. The 80/20 scrape targets that catch the important failures:
# prometheus.yml — the essentialsscrape_configs: - job_name: "node" static_configs: - targets: ["localhost:9100"]
- job_name: "docker" static_configs: - targets: ["localhost:9323"] # Docker engine metrics
- job_name: "blackbox" metrics_path: /probe params: module: [http_2xx] static_configs: - targets: - "https://git.example.com" # is my Git server up? - "https://media.example.com" # is the media stack up? - "https://backup.example.com" # is the dashboard up? relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instanceThe blackbox exporter is the sleeper hit: it probes your services from the outside, exactly the way your users experience them. "The container is running" and "the service responds" are different facts, and blackbox checks the second one.
The alert that's actually useful
The first alert everyone writes is "CPU > 90%." It's also the first alert everyone mutes, because a CPU spike that recovers in a minute is noise. The alerts that earn their keep are the ones about availability and trends:
# prometheus.yml — alerting rulesgroups: - name: homelab rules: - alert: ServiceDown expr: probe_success == 0 for: 2m labels: severity: critical annotations: summary: "{{ $labels.instance }} is down"
- alert: DiskFilling expr: disk_used_bytes / disk_total_bytes > 0.85 for: 30m labels: severity: warning annotations: summary: "{{ $labels.mountpoint }} > 85%"Note the for: clauses. for: 2m means "down for two consecutive minutes" — a blip during a restart doesn't page you. for: 30m on disk means the alert fires on a trend, not a moment, which is exactly when you still have time to act. If your alerts fire on moments, you'll mute them; if they fire on trends, you'll fix things.
The dashboard philosophy
Grafana dashboards are where monitoring goes to die if you're not careful — 40 panels of lines nobody reads. My rules after a year:
- Three panels per service, max. Uptime, resource usage, and a trend. If a service needs ten panels, it needs its own dashboard.
- Red/green, not rainbow. The dashboard should tell you at a glance what's wrong. Everything healthy = green; anything broken = red; the in-between is for clicking into.
- Alert-backed, not dashboard-driven. The dashboard is for after the alert wakes you up. The alert is the system that's actually watching.
Logs with Loki: cheap and searchable
Loki is the log database that respects a 16 GB budget: it indexes labels, not full text, so it stays small and fast on modest hardware. Promtail ships container logs to it, and the payoff is the query that saved me twice already:
{job="docker"} |= "panic" |= "media-server""Show me every panic in the media server's logs, ever." On Elasticsearch that's a whole cluster; on Loki it's a query against a few hundred MB. For homelab scale, Loki is the correct answer, full stop.
The lessons
A year in, the stack has caught: a disk filling from a runaway backup (30-minute warning alert, fixed at leisure), a reverse-proxy cert that stopped renewing (blackbox probe went red, fixed in five minutes), and one actual crash (ServiceDown alert at 3 a.m., which I silenced and investigated at 8 a.m. — the alert did its job: I chose when to deal with it).
The honest lessons:
- Start with blackbox probes. Before metrics, before logs — know when your services stop responding. Everything else is refinement.
- Alert on trends, not moments.
for: 2mandfor: 30mare the difference between alerts you act on and alerts you mute. - One weekend to set up, forever to tune. The initial stack took an afternoon; the tuning — thresholds, alert fatigue, dashboard pruning — is the ongoing cost, and it's worth paying.
- The stack must be boring. No beta software, no hand-rolled exporters, no experiment-as-infrastructure. The monitoring system that breaks is worse than no monitoring system.
The takeaway
Observability on a homelab budget is not a compromise — it's a discipline problem, and the free stack solves it completely. Metrics for health, logs for history, alerts for trends, all in one compose file on the box that was already running the services. The best part isn't the dashboards or the notifications. It's the peace of mind that comes from knowing your machines will tell you when they're sick — instead of finding out the way I used to, from the outside, when everything was already down.