Running a homelab without proper monitoring is flying blind. You don’t know when your disk is about to fill up, when your RAM spikes, or when a container silently dies — until something breaks. Grafana and Prometheus fix that. Together, they give you gorgeous real-time dashboards and deep metrics for every server, container, and service you run.

This guide walks you through setting up a complete Grafana + Prometheus monitoring stack using Docker Compose. By the end, you’ll have live dashboards showing CPU, RAM, disk, network, and container stats — all self-hosted, no cloud dependency.

💡 This article contains affiliate links. If you buy through them, we earn a small commission at no extra cost to you. Learn more.

What Is Grafana and Prometheus?

Before diving into config files, a quick primer on how these two tools work together:

  • Prometheus is a time-series database and metrics scraper. It pulls (“scrapes”) metrics from targets at regular intervals and stores them. Think of it as the data engine.
  • Grafana is the visualization layer. It connects to Prometheus (and many other data sources) and renders that data as interactive dashboards.
  • Exporters are small agents you install on hosts or services to expose metrics in a format Prometheus understands. The most common is Node Exporter for Linux host metrics.

The flow is simple: Node Exporter → Prometheus → Grafana. Prometheus scrapes exporters; Grafana queries Prometheus; you see pretty graphs.


Prerequisites

  • A server or homelab machine running Linux (Ubuntu 22.04+ recommended)
  • Docker and Docker Compose installed
  • Basic familiarity with Docker Compose files
  • Ports 3000 (Grafana) and 9090 (Prometheus) available

If you’re running a mini PC or small server for your homelab, a machine with at least 4GB RAM works well. Popular choices like the Beelink Mini S12 Pro or MINISFORUM UM350 handle this stack without breaking a sweat.


Step 1 — Create the Project Directory

Set up a clean directory structure:

1
2
mkdir -p ~/monitoring/{prometheus,grafana/provisioning/{datasources,dashboards}}
cd ~/monitoring

Your directory tree will look like:

~/monitoring/
├── docker-compose.yml
├── prometheus/
│   └── prometheus.yml
└── grafana/
    └── provisioning/
        ├── datasources/
        │   └── prometheus.yml
        └── dashboards/
            └── dashboards.yml

Step 2 — Write the Docker Compose File

Create docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
version: "3.8"

networks:
  monitoring:
    driver: bridge

volumes:
  prometheus_data: {}
  grafana_data: {}

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--web.console.libraries=/usr/share/prometheus/console_libraries"
      - "--web.console.templates=/usr/share/prometheus/consoles"
      - "--storage.tsdb.retention.time=30d"
      - "--web.enable-lifecycle"
    ports:
      - "9090:9090"
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=changeme
      - GF_USERS_ALLOW_SIGN_UP=false
      - GF_SERVER_ROOT_URL=https://grafana.yourdomain.com
    ports:
      - "3000:3000"
    networks:
      - monitoring
    depends_on:
      - prometheus

  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    restart: unless-stopped
    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)($$|/)"
    ports:
      - "9100:9100"
    networks:
      - monitoring

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    restart: unless-stopped
    privileged: true
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    ports:
      - "8080:8080"
    networks:
      - monitoring

Key containers explained:

ContainerRole
prometheusMetrics database + scraper
grafanaDashboard visualization
node-exporterHost OS metrics (CPU, RAM, disk, network)
cadvisorDocker container metrics

Change the Grafana password! Replace changeme with a strong password before starting.


Step 3 — Configure Prometheus

Create prometheus/prometheus.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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"]

  - job_name: "cadvisor"
    static_configs:
      - targets: ["cadvisor:8080"]

This tells Prometheus to scrape metrics from itself, the Node Exporter, and cAdvisor every 15 seconds. The --storage.tsdb.retention.time=30d flag in the Compose file keeps 30 days of history — adjust to taste based on your available disk space.

For reference: a typical homelab setup generates roughly 1–2 GB of Prometheus data per month. Even a small SSD in your server handles this easily.


Step 4 — Configure Grafana Auto-Provisioning

Grafana supports automatic data source and dashboard provisioning. This means your setup is reproducible — no clicking through the UI every time.

Create grafana/provisioning/datasources/prometheus.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    jsonData:
      httpMethod: POST
      exemplarTraceIdDestinations: []

Create grafana/provisioning/dashboards/dashboards.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: 1

providers:
  - name: "default"
    orgId: 1
    folder: ""
    type: file
    disableDeletion: false
    updateIntervalSeconds: 30
    options:
      path: /etc/grafana/provisioning/dashboards

Step 5 — Start the Stack

1
2
cd ~/monitoring
docker compose up -d

Check all containers are running:

1
docker compose ps

Expected output:

NAME             STATUS         PORTS
cadvisor         Up             0.0.0.0:8080->8080/tcp
grafana          Up             0.0.0.0:3000->3000/tcp
node-exporter    Up             0.0.0.0:9100->9100/tcp
prometheus       Up             0.0.0.0:9090->9090/tcp

Step 6 — Verify Prometheus Targets

Open Prometheus at http://your-server-ip:9090/targets. You should see all three targets (prometheus, node-exporter, cadvisor) with a green UP status.

If a target shows DOWN, check:

  1. Container is running: docker compose logs node-exporter
  2. Network connectivity: all containers share the monitoring bridge network
  3. Firewall rules: ensure the exporter ports aren’t blocked between containers (Docker handles this internally, but check if you have custom iptables rules)

Step 7 — Set Up Grafana Dashboards

Open Grafana at http://your-server-ip:3000 and log in with admin / your password.

The Prometheus data source is already configured via provisioning. Now import community dashboards instead of building from scratch:

Node Exporter Dashboard (ID: 1860)

This is the gold standard for host metrics:

  1. Go to Dashboards → Import
  2. Enter dashboard ID: 1860
  3. Select your Prometheus data source
  4. Click Import

You’ll instantly have CPU usage, memory, disk I/O, network throughput, system load, and uptime — all broken down and graphed beautifully.

Docker / cAdvisor Dashboard (ID: 193)

For container-level metrics:

  1. Dashboards → Import
  2. Enter dashboard ID: 193
  3. Select Prometheus
  4. Import

This shows per-container CPU, memory, network, and block I/O. Extremely useful for spotting runaway containers.


Prometheus has a companion tool called Alertmanager for routing alerts. But for a simpler setup, Grafana’s built-in alerting works great.

Create an Alert in Grafana

  1. Open the Node Exporter dashboard
  2. Find the Disk Space Used panel
  3. Edit → Alert tab → Create alert rule

Example alert for disk space:

Condition: WHEN avg() OF A IS ABOVE 85
For: 5m
Notifications: send to [your notification channel]

Configure Notification Channels

Grafana supports email, Telegram, Slack, PagerDuty, and many more. For homelab use, Telegram is popular — it’s free and instant.

Go to Alerting → Contact points → Add contact point, select Telegram, and paste your bot token + chat ID.


Step 9 — Monitor Multiple Hosts

Got more than one machine to monitor? Node Exporter runs on each host and reports back to the central Prometheus instance.

On each additional host:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
docker run -d \
  --name node-exporter \
  --restart unless-stopped \
  -v /proc:/host/proc:ro \
  -v /sys:/host/sys:ro \
  -v /:/rootfs:ro \
  -p 9100:9100 \
  prom/node-exporter:latest \
  --path.procfs=/host/proc \
  --path.sysfs=/host/sys \
  --collector.filesystem.mount-points-exclude="^/(sys|proc|dev|host|etc)($|/)"

Then add the new host to prometheus/prometheus.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  - job_name: "node-exporter"
    static_configs:
      - targets:
          - "node-exporter:9100"       # local
          - "192.168.1.100:9100"       # NAS
          - "192.168.1.101:9100"       # second server
    relabel_configs:
      - source_labels: [__address__]
        regex: "([^:]+):.*"
        target_label: instance
        replacement: "$1"

Reload Prometheus config without restarting:

1
curl -X POST http://localhost:9090/-/reload

This works because of the --web.enable-lifecycle flag we set earlier.


Step 10 — Secure Your Monitoring Stack

If you’re exposing Grafana to the internet (or even just your local network), a few security steps matter:

Change Default Credentials

Update the Grafana admin password immediately:

1
2
# Via environment variable in docker-compose.yml
GF_SECURITY_ADMIN_PASSWORD=your-strong-password-here

Or via the Grafana UI: Profile → Change Password.

Put Grafana Behind a Reverse Proxy

Don’t expose Grafana directly. Use Nginx Proxy Manager, Traefik, or Caddy to terminate HTTPS and proxy to port 3000. This gives you a clean URL like https://grafana.yourdomain.com with a valid SSL certificate.

Update the GF_SERVER_ROOT_URL environment variable to match.

Keep Prometheus Internal

Prometheus has no built-in authentication. Never expose port 9090 to the internet. Keep it internal, accessible only by Grafana and your local network. If you need remote access, use Tailscale or a VPN.


Adding More Exporters

The real power of Prometheus comes from its exporter ecosystem. Once your base stack is running, you can plug in dozens of exporters to monitor virtually anything.

Blackbox Exporter — HTTP/TCP Uptime Checks

The Blackbox Exporter lets Prometheus probe endpoints externally — like a lightweight uptime checker built right into your monitoring stack.

Add it to your docker-compose.yml:

1
2
3
4
5
6
7
8
  blackbox-exporter:
    image: prom/blackbox-exporter:latest
    container_name: blackbox-exporter
    restart: unless-stopped
    ports:
      - "9115:9115"
    networks:
      - monitoring

Then in prometheus.yml, add a scrape config for your sites:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  - job_name: "http-checks"
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - https://selfhostwise.com
          - https://yourservice.domain.com
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115

Now Prometheus checks your URLs every 15 seconds and alerts you if they go down.

SNMP Exporter — Network Gear Metrics

If you have managed switches or routers that support SNMP (Ubiquiti, MikroTik, etc.), the SNMP Exporter pulls port throughput, error rates, and interface stats directly from the hardware — no agent needed on the device.

PostgreSQL Exporter

Running Postgres? Add the prometheuscommunity/postgres-exporter container to your stack and monitor query throughput, connection pool saturation, replication lag, and table bloat.


Using Grafana Loki for Log Aggregation

Grafana’s ecosystem extends beyond metrics. Loki is a log aggregation system designed to work natively with Grafana — think Elasticsearch but lighter and cheaper to run.

With Loki + Promtail (the log shipper), you can:

  • View logs alongside metrics in the same Grafana dashboard
  • Search logs with LogQL (similar to PromQL)
  • Correlate a CPU spike with the exact log lines that caused it
  • Set alerts on log patterns (e.g., “alert me if I see more than 10 auth failures per minute”)

Add Loki and Promtail to your stack:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  loki:
    image: grafana/loki:latest
    container_name: loki
    restart: unless-stopped
    ports:
      - "3100:3100"
    networks:
      - monitoring
    command: -config.file=/etc/loki/local-config.yaml

  promtail:
    image: grafana/promtail:latest
    container_name: promtail
    restart: unless-stopped
    volumes:
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
    networks:
      - monitoring
    command: -config.file=/etc/promtail/config.yml

Once Loki is connected as a data source in Grafana, you get a unified observability platform: metrics, logs, and (with Tempo) traces — all in one place.


Backing Up Your Monitoring Data

After investing time in building dashboards and tuning alerts, you want to protect that work. Here’s what to back up:

Grafana: All dashboards, data sources, and settings live in the grafana_data Docker volume. Back this up regularly:

1
2
3
4
docker run --rm \
  -v monitoring_grafana_data:/data \
  -v /backup:/backup \
  alpine tar czf /backup/grafana-$(date +%Y%m%d).tar.gz /data

Prometheus: Time-series data is in prometheus_data. For most setups, you don’t need to back up Prometheus itself — the data is ephemeral and regenerates. What matters is backing up your prometheus.yml config and any recording rules. Store those in Git.

Export Grafana Dashboards as JSON: In Grafana, go to any dashboard → Share → Export → Save to file. Keep these JSON files in a Git repo. You can re-import them instantly on a fresh install.


Useful Prometheus Queries (PromQL)

Once your stack is running, here are some handy PromQL queries to build custom panels:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# CPU usage percentage (all cores)
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# RAM used
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100

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

# Network receive rate (bytes/sec)
rate(node_network_receive_bytes_total{device!="lo"}[5m])

# Number of running Docker containers
count(container_last_seen{name!=""})

# Top 5 memory-hungry containers
topk(5, container_memory_usage_bytes{name!=""})

These form the building blocks of custom dashboards tailored to your setup.


Troubleshooting Common Issues

Grafana shows “No data” on panels

  • Check Prometheus targets page — all targets must be UP
  • Verify the data source URL in Grafana points to http://prometheus:9090
  • Check time range — make sure you’re not looking at “last 5 minutes” with no recent data

Node Exporter shows wrong disk stats

  • Adjust the --collector.filesystem.mount-points-exclude regex to include your actual mount points
  • For ZFS or Btrfs filesystems, add additional collector flags

cAdvisor can’t access Docker socket

  • Make sure privileged: true is set in the Compose file
  • Check that /var/lib/docker is accessible from the container

High memory usage from Prometheus

  • Reduce scrape frequency from 15s to 30s for less-critical metrics
  • Reduce retention time from 30d to 15d
  • Add recording rules to pre-aggregate expensive queries

Storage Considerations

For a serious homelab with multiple hosts, consider the storage requirements:

  • Prometheus data: ~1–2 GB/month per host with default settings
  • Grafana: minimal (dashboard configs only, actual data lives in Prometheus)
  • Minimum recommended: 20 GB SSD for a 3-host setup with 30-day retention

If you’re building out your storage, a fast SSD matters more than raw capacity here. The Samsung 870 EVO or WD Red SA500 are solid NAS-grade options that handle the random I/O Prometheus generates well.


Wrapping Up

You now have a full-featured monitoring stack running:

  • ✅ Prometheus scraping host and container metrics every 15 seconds
  • ✅ Node Exporter reporting CPU, RAM, disk, and network
  • ✅ cAdvisor tracking every Docker container
  • ✅ Grafana with beautiful dashboards
  • ✅ Optional alerting for disk, CPU, or memory thresholds

This stack scales surprisingly well — from a single Raspberry Pi to a multi-host homelab with dozens of services. And because everything runs in Docker Compose, it’s portable: commit your config to Git and you can rebuild the entire monitoring setup on a new machine in under five minutes.

For next steps, consider adding exporters for specific services: Blackbox Exporter for endpoint uptime checks, PostgreSQL Exporter for database metrics, or Loki (Grafana’s log aggregation tool) to add log search alongside your metrics dashboards.


Found this guide useful? Share it with your homelab community — and let us know in the comments what you’re monitoring!