The first time most people find out a self-hosted service is down is when someone tries to use it and it isn’t there. That’s the wrong order. If you’re running more than two or three services at home - Immich, a media server, a reverse proxy, whatever - you’ve already got enough surface area for something to quietly die on a Tuesday and stay dead until you notice by accident.

Monitoring fixes that, but “monitoring” in the homelab world actually means two different things that get conflated constantly: uptime checking (is this thing responding right now) and metrics collection (what is this thing actually doing over time - CPU, memory, disk, request rates). You don’t need a full observability stack to get real value. You need one tool for each job, wired together sensibly, and an alert that reaches you somewhere you’ll actually see it.

The two-tool split, and why one tool doesn’t do both well

Uptime Kuma answers “is it up.” You give it a URL, a port, a ping target, or a Docker container name, and it checks on an interval - 60 seconds, 5 minutes, whatever you set. When a check fails, it fires a notification. That’s the entire job, and it does it well: clean web UI, dozens of notification integrations built in, status pages if you want to show something public, and a Docker image that takes about five minutes to stand up.

Grafana, paired with Prometheus as the data source, answers “what’s actually happening.” Prometheus scrapes numeric metrics from your services and hosts on a regular interval and stores them as time series. Grafana turns that time series into dashboards - CPU trending up over three weeks, memory that never gets released after a restart, disk filling faster than it should. Uptime Kuma tells you a service died. Grafana tells you it was dying for two weeks before it did.

People try to make one tool do both jobs and end up disappointed either way. Uptime Kuma has a basic metrics/history view, but it’s not built for real time-series analysis. Prometheus and Grafana can technically do uptime probing (via blackbox_exporter), but the setup is heavier than it needs to be for something as simple as “ping this every minute.” Run both, let each do what it’s actually good at.

Setting up Uptime Kuma

This is the easy half. A single-container Docker Compose stack:

services:
  uptime-kuma:
    image: louislam/uptime-kuma:1
    container_name: uptime-kuma
    volumes:
      - ./data:/app/data
    ports:
      - "3001:3001"
    restart: unless-stopped

Bring it up, hit the web UI, create your admin account on first login, and start adding monitors. For each service you care about, add a monitor of the matching type:

  • HTTP(s) for anything with a web UI - point it at the actual login page, not just the root domain, so a “page loads but auth is broken” scenario still gets caught.
  • TCP port for things without HTTP, like a database or an SSH daemon you want to confirm is listening.
  • Docker container if Uptime Kuma has access to the Docker socket - it can check container health directly rather than poking at a port.
  • Ping for raw host reachability, useful for things like your router or a NAS that doesn’t run a web service you can hit.

Set the check interval per monitor, not globally. A reverse proxy or auth service that everything else depends on deserves a 30-60 second interval. A low-stakes internal tool checked once every 5 minutes is fine and saves you from a monitor list that hammers your network for no reason.

The part that actually matters is notifications. Uptime Kuma supports dozens of targets out of the box - Discord, Telegram, ntfy, Pushover, generic webhooks, email. Pick something that reaches you outside your homelab network, because if your internet or your core router is what went down, a notification that only works over LAN never arrives. Discord or a phone-push service like ntfy/Pushover both clear that bar cheaply.

One setting worth changing from the default: retries before alert. Straight out of the box, Uptime Kuma can fire on the very first failed check, which turns a single dropped packet into a 3am notification. Set retries to 2-3 with a short interval between them before it actually alerts - real outages persist past one bad check, transient blips don’t.

Setting up Prometheus and Grafana

This stack has more moving parts, but the shape is standard and doesn’t change much between setups. Three pieces:

  1. Prometheus - the time-series database and scraper.
  2. Exporters - small agents that expose metrics in a format Prometheus understands. node_exporter for host-level stats (CPU, memory, disk, network) runs on every machine you want visibility into. Most self-hosted apps that care about observability ship their own /metrics endpoint already (Lemmy and Immich both do, for example).
  3. Grafana - the dashboard layer that queries Prometheus and renders it.

A minimal docker-compose.yaml for the Prometheus + Grafana half:

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus-data:/prometheus
    ports:
      - "9090:9090"
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    volumes:
      - ./grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"
    restart: unless-stopped

And a prometheus.yml that scrapes a node exporter and itself:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']
  - job_name: 'node'
    static_configs:
      - targets: ['10.0.50.10:9100']

If you’re running any of this inside an LXC container rather than a full VM, the usual gotcha applies: Docker’s default bridge networking can fight with LXC’s confinement (permission errors on network-namespace sysctls). Setting network_mode: host on the containers sidesteps it cleanly and is the standard fix for Docker-in-LXC setups generally.

Once Prometheus is scraping, add it as a data source in Grafana (http://prometheus:9090 if they’re on the same Docker network, or the host IP and port otherwise) and import a pre-built dashboard rather than building one from scratch. The community dashboard library at grafana.com has solid ready-made options for node_exporter specifically - search for the node exporter full dashboard and import it by ID. You’ll have CPU, memory, disk, and network graphs for every host in a couple of minutes instead of an afternoon.

Alerting on metrics, not just uptime

The real payoff of the Prometheus/Grafana half isn’t the dashboards, it’s catching trouble before it becomes an outage. Grafana’s alerting can watch a Prometheus query and fire a notification when it crosses a threshold - disk usage over 85%, a host’s load average sustained above its core count for 5+ minutes, memory usage climbing without ever coming back down after a restart (a classic slow-leak signature).

Start with two or three alerts that map to things that have actually bitten you before, rather than trying to cover everything on day one. A disk-almost-full alert and a “this container’s CPU has been pegged for 5 minutes straight” alert cover a large share of real homelab incidents by themselves. Route these to the same notification channel as Uptime Kuma so there’s one place you check, not two.

What this buys you

None of this prevents outages. What it does is change the moment you find out about one from “someone complains” or “you happen to open the app” to “a notification lands on your phone within a minute or two.” For a single-operator homelab, that’s most of the value observability tooling exists to provide - not root-cause analysis, not SLA dashboards, just the basic guarantee that silence means things are actually fine, not that nobody’s looked lately.

Skip the temptation to monitor everything on day one. Wire up Uptime Kuma for the handful of services you’d actually be upset to lose without knowing, add Prometheus and Grafana once that’s stable, and expand from there as you find gaps - usually right after the first outage that gets past you undetected. That one will happen either way. The goal is making sure it only happens once.