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
- A machine running Docker and Docker Compose v2 (any recent Linux distro is fine)
- Ports 3001 (Uptime Kuma) and 3000 (Grafana) free, or a reverse proxy handling those — I use Caddy
- Basic familiarity with editing YAML files
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
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:
- HTTP(s) monitor for each self-hosted web app (Nextcloud, Gitea, Jellyfin, etc.)
- TCP Port monitor for services that don't have a web frontend — e.g., WireGuard on port 51820
- DNS monitor to check that your internal DNS resolver is responding
- Ping monitor for your router/gateway — tells you immediately if you've lost LAN connectivity rather than just a single service
- Push monitor combined with a cron job on each host — if the cron stops sending heartbeats, you know the machine is down
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.
Discussion