How to Set Up Grafana and Prometheus (Proper Dashboards for Your Server)

Grafana + Prometheus: three months of graphs — Netdata is now. This is history.

Netdata (tutorial 66) is the live view. Prometheus and Grafana are the history: metrics collected every 15 seconds and kept for months, with dashboards that show you how memory use crept up over the last quarter, which container's disk I/O spikes at 2 am, and whether the new SSD actually helped. It's the same stack the big outfits use, scaled down to one compose file.

Quick reference

ComponentJobPort
node_exporterserver metrics (CPU, memory, disk, network, temperatures)9100
cAdvisorper-container metrics8081
Prometheuscollects and stores them9090
Grafanadraws them3030
Ready dashboardsGrafana → Import → ID 1860 (node), 14282 (cAdvisor)

1. The compose file

mkdir -p ~/docker/monitoring && cd ~/docker/monitoring
nano compose.yaml

services:
node_exporter:
image: prom/node-exporter:latest
container_name: node_exporter
command: ["--path.rootfs=/host"]
pid: host
volumes:
- /:/host:ro,rslave
restart: unless-stopped

cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
privileged: true
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
restart: unless-stopped

prometheus:
image: prom/prometheus:latest
container_name: prometheus
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=90d
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus-data:/prometheus
restart: unless-stopped

grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3030:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
volumes:
- ./grafana-data:/var/lib/grafana
restart: unless-stopped

Only Grafana is published; the rest talk on the compose network. Put GRAFANA_PASSWORD=... in .env.

2. Tell Prometheus what to scrape

nano prometheus.yml

global:
scrape_interval: 15s

scrape_configs:
- job_name: node
static_configs:
- targets: ["node_exporter:9100"]
- job_name: cadvisor
static_configs:
- targets: ["cadvisor:8080"]
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]

mkdir -p prometheus-data grafana-data && sudo chown 65534:65534 prometheus-data && sudo chown 472:472 grafana-data
docker compose up -d
sudo ufw allow from 192.168.1.0/24 to any port 3030

3. Grafana

Open http://192.168.1.50:3030, log in as admin with your password.

  1. Connections → Data sources → Add → Prometheus, URL http://prometheus:9090, Save & test.
  2. Dashboards → New → Import, type 1860, choose the Prometheus source, Import. That's "Node Exporter Full": CPU, memory, disk, network, temperatures, over any time range.
  3. Import 14282 for a per-container dashboard from cAdvisor.

Pick Last 7 days at the top right and watch patterns appear that no live view shows.

4. More exporters

Anything with a /metrics endpoint can be scraped. Add a job per thing:

5. One alert

Alerting → Alert rules → New: query node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.1, evaluate every minute, for 5m. Then Contact points → New → Telegram with your bot token and chat ID, and set it as the default policy. Disk under 10% → message. Same pattern for memory, load, a container's restarts.

Grafana, Netdata, Uptime Kuma

Kuma: is it up? Netdata: what's happening right now? Grafana: what happened over the last three months? A home server can run all three; Grafana is the one you'll look at least often and be gladdest to have.

Troubleshooting

Grafana says "data source not working". Wrong URL — it's http://prometheus:9090 (the service name), not localhost.

Dashboard panels say "No data". Prometheus isn't scraping: http://server-ip:9090/targets (publish the port temporarily, or docker compose exec prometheus wget -qO- localhost:9090/targets). Usually a typo in the job's target.

"permission denied" in the Prometheus or Grafana logs. The data folders need the right owner — the chown lines above (65534 for Prometheus, 472 for Grafana).

Disk filling. Retention is 90 days by default here; lower --storage.tsdb.retention.time, or raise scrape_interval to 30s.

Wrapping up

Four containers, one scrape config, two imported dashboards. The server's history becomes visible, "it feels slower than last month" becomes a graph, and the alert that tells you about a full disk arrives a week before it's a problem.

Call us