Monitoring Your Homelab with Uptime Kuma and Grafana in Docker

Monitoring Your Homelab with Uptime Kuma and Grafana in Docker

We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.

Running a homelab without monitoring is like driving blindfolded — everything feels fine right up until it isn't. I learned this the hard way when my Vaultwarden instance went down for six hours and I only noticed because I couldn't log into something. These days I run both Uptime Kuma for at-a-glance service health and Grafana (backed by Prometheus) for deep-dive metrics, and the entire stack lives in a single Docker Compose file. In this tutorial I'll walk you through building that exact setup from scratch.

Why Two Tools Instead of One?

Uptime Kuma is purpose-built for uptime monitoring. It gives you beautiful status pages, per-service response-time graphs, and push notifications to Telegram, Discord, Slack, or email in about five minutes of configuration. Grafana, on the other hand, is a general-purpose metrics dashboard. When I want to know whether my Jellyfin server is CPU-throttling at 2 AM or how much RAM my Nextcloud instance has been chewing through over the last week, I reach for Grafana.

The two tools complement each other perfectly. Uptime Kuma answers "is it up?" Grafana answers "why is it slow?" I run both, and I think you should too.

Prerequisites

Tip: If you're running this on a VPS rather than local hardware, make sure those ports are allowed through your firewall (ufw allow 3000/tcp and ufw allow 3001/tcp) before you start. You can tighten this up later by putting everything behind a reverse proxy and closing the direct ports again.

Project Structure

I keep all my monitoring config in ~/docker/monitoring/. Here's the directory tree we're going to build:

mkdir -p ~/docker/monitoring/grafana/provisioning/datasources
mkdir -p ~/docker/monitoring/grafana/provisioning/dashboards
mkdir -p ~/docker/monitoring/prometheus
cd ~/docker/monitoring

The Docker Compose File

This single Compose file brings up Uptime Kuma, Prometheus, and Grafana. I also include Node Exporter because Grafana without system metrics feels incomplete — it's a tiny container and costs almost nothing.

cat > ~/docker/monitoring/docker-compose.yml << 'EOF'
version: "3.8"

networks:
  monitoring:
    driver: bridge

volumes:
  uptime-kuma-data:
  prometheus-data:
  grafana-data:

services:

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

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.retention.time=30d"
    networks:
      - monitoring

  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    restart: unless-stopped
    pid: host
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - "--path.procfs=/host/proc"
      - "--path.sysfs=/host/sys"
      - "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
    networks:
      - monitoring

  grafana:
    image: grafana/grafana-oss:latest
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=changeme
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    networks:
      - monitoring

EOF
Watch out: The GF_SECURITY_ADMIN_PASSWORD=changeme environment variable only sets the password on the very first start. If Grafana has already initialised its database, changing this value in Compose won't update the password — you'll need to reset it through the Grafana UI under Profile → Change Password. Set a strong password before you expose this to a network.

Prometheus Configuration

Prometheus needs to know what to scrape. This minimal config scrapes itself and Node Exporter. You can add more targets later — I have separate jobs for cAdvisor, Blackbox Exporter, and individual application metrics.

cat > ~/docker/monitoring/prometheus/prometheus.yml << 'EOF'
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:

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

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

EOF

Grafana Datasource Provisioning

Rather than clicking through the Grafana UI every time I rebuild the stack, I provision the Prometheus datasource automatically using a YAML file. Grafana reads this on startup and creates the datasource without any manual intervention.

cat > ~/docker/monitoring/grafana/provisioning/datasources/prometheus.yml << 'EOF'
apiVersion: 1

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

EOF

Starting the Stack

With everything in place, bring the stack up:

cd ~/docker/monitoring
docker compose up -d

# Watch logs to make sure everything is healthy
docker compose logs -f --tail=50

After about 30 seconds you should be able to reach Uptime Kuma at http://your-host:3001 and Grafana at http://your-host:3000. Uptime Kuma will ask you to create an account on first launch — do that immediately, since the setup page is publicly accessible until you do.

Setting Up Uptime Kuma Monitors

Once you're logged in to Uptime Kuma, click Add New Monitor. For a web service I use type HTTP(s) and point it at the internal Docker network name if the service is on the same host, or the external URL if it's elsewhere. I set a heartbeat interval of 60 seconds for most things and drop it to 30 seconds for anything critical like my VPN gateway.

The notification setup is where Uptime Kuma shines. I have it posting to a private Telegram channel for immediate alerts and a Discord webhook for my homelab server where I can see the history. Go to Settings → Notifications, pick your platform, paste in your bot token or webhook URL, and you're done. Test it — don't assume it works.

Importing a Grafana Dashboard for Node Exporter

Building dashboards from scratch takes time. The community dashboard Node Exporter Full (ID 1860) is excellent and covers CPU, memory, disk I/O, network throughput, and filesystem usage in one import. In Grafana go to Dashboards → Import, type 1860 in the Grafana.com dashboard field, click Load, select your Prometheus datasource, and click Import. You'll have a production-quality system dashboard in under a minute.

I also import dashboard 193 for Docker container metrics once I add cAdvisor to the stack — but that's a topic for another post.

Keeping the Stack Updated with Watchtower

I add Watchtower to almost every stack I run so I don't have to manually pull new images. For monitoring tools, staying current matters because Uptime Kuma and Grafana both release security patches fairly regularly. You can add this service to the same Compose file:

# Add this service block to your docker-compose.yml services section
  watchtower:
    image: containrrr/watchtower
    container_name: watchtower-monitoring
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 86400 --cleanup uptime-kuma prometheus grafana node-exporter

The --interval 86400 flag checks for updates once per day, and listing the container names at the end means Watchtower only touches these specific containers rather than everything on the host.

Putting It All Behind Caddy

I prefer not to expose raw ports to the internet. My Caddyfile entries for this stack look like this — swap the hostnames for your own domains or internal DNS names:

status.yourdomain.com {
    reverse_proxy localhost:3001
}

grafana.yourdomain.com {
    reverse_proxy localhost:3000
}

Caddy handles Let's Encrypt certificates automatically, so both services get HTTPS with zero extra configuration on your part. If you're running on a VPS and want a solid cloud host to run this on, DigitalOcean is my go-to for reliable Droplets with predictable pricing . A $6/month Droplet is more than enough for a monitoring stack like this.

Useful Checks to Add Immediately

Once Uptime Kuma is running, here are the monitors I add on every new homelab setup:

Tip: Use Uptime Kuma's built-in Status Page feature to create a public or private status page you can share with family or teammates. It's polished enough that I've used it on small client projects instead of paid status page services.

Next Steps

This stack gives you a solid foundation: Uptime Kuma handling availability alerting and Grafana giving you the metrics depth to actually diagnose problems. From here I'd recommend adding cAdvisor (image: gcr.io/cadvisor/cadvisor:latest) to get per-container CPU and memory metrics in Grafana, and wiring up Grafana's alerting engine to your notification channels as a second layer on top of Uptime Kuma. If you want to add authentication in front of both tools rather than relying on their built-in login screens, check out the Authelia tutorial elsewhere on this site — it slots neatly into the same Caddy setup described above.

DigitalOcean

Discussion