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

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
| Component | Job | Port |
|---|---|---|
| node_exporter | server metrics (CPU, memory, disk, network, temperatures) | 9100 |
| cAdvisor | per-container metrics | 8081 |
| Prometheus | collects and stores them | 9090 |
| Grafana | draws them | 3030 |
| Ready dashboards | Grafana → 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.
- Connections → Data sources → Add → Prometheus, URL http://prometheus:9090, Save & test.
- Dashboards → New → Import, type 1860, choose the Prometheus source, Import. That's "Node Exporter Full": CPU, memory, disk, network, temperatures, over any time range.
- 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:
- Pi-hole: ekofr/pihole-exporter → dashboard 10176
- Nginx: nginx/nginx-prometheus-exporter with stub_status on
- SMART: prometheuscommunity/smartctl-exporter → drive temperatures and health over time
- Speedtest: miguelndecarvalho/speedtest-exporter → is the ISP delivering?
- Uptime Kuma has a built-in /metrics (API key under settings)
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.
