Grafana Dashboards - Visualizing Your Metrics
Status: Active
Last Updated: 2026-08-26
Category: Observability - Phase 3: Visualization
Prerequisites: prometheus-basics, netdata-basics
Time: 2-3 hours
Tags: grafana, dashboards, prometheus, provisioning, panels, templating, alerting, docker
Summary
Grafana is the visualization layer of a metrics stack: point it at your Prometheus from prometheus-basics, and every query becomes a panel, every panel a dashboard. This guide covers installing Grafana in Docker, adding the Prometheus datasource, importing battle-tested community dashboards (Node Exporter Full & friends), building custom panels with PromQL, using template variables to make one dashboard serve many hosts, configuring Grafana-managed alert rules with notification contact points, and β most importantly β provisioning all of it as code so a rebuilt server reassembles itself from files in Git.
π― What You'll Learn
By the end of this article, you'll be able to:
- β Run Grafana in Docker with sane volume mounts
- β Add a Prometheus datasource (UI and provisioned)
- β Import pre-built community dashboards by ID
- β Build custom panels with PromQL (timeseries, stat, gauge, table)
- β Create dashboard variables for host/service switching
- β Write Grafana alert rules with proper notification routing
- β Provision datasources + dashboards as versioned YAML/JSON
Table of Contents
- Why Grafana
- Docker Installation
- Adding the Prometheus Datasource
- Pre-Built Dashboards
- Building Custom Panels
- Variables & Templating
- Alerting in Grafana
- Provisioning as Code
- Troubleshooting & Common Pitfalls
- Sources & Related
Why Grafana
Prometheus ships a functional graph page, but it's a lab bench: no persistent dashboards, no variables, no sharing, no alert UI. Grafana adds the presentation layer:
| Capability | Prometheus built-in | Grafana |
|---|---|---|
| Ad-hoc PromQL graphs | β | β |
| Saved, organized dashboards | β | β |
| Template variables (host dropdowns) | β | β |
| Mixed datasources on one board | β | β |
| Alert rules + routing + silences | via Alertmanager | β unified |
| Public links / snapshots / RBAC | β | β |
Grafana doesn't store metrics β it queries Prometheus on every page load. That means deleting a Grafana dashboard loses only the dashboard, never data.
Docker Installation
services:
grafana:
image: grafana/grafana-oss:latest
container_name: grafana
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} # from .env
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=https://grafana.example.com
volumes:
- ./data:/var/lib/grafana # sqlite DB, plugins, dashboards state
- ./provisioning:/etc/grafana/provisioning # as-code configs (below)
echo "GRAFANA_ADMIN_PASSWORD=change-me-long-random" >> .env
mkdir -p data provisioning/datasources provisioning/dashboards
sudo chown -R 472:472 data # grafana runs as uid 472
docker compose up -d
curl -I http://127.0.0.1:3000 # HTTP 200
Log in at http://<server>:3000 with the credentials above, then change the password immediately if you ever plan to expose this beyond localhost.
π Same rule as Netdata/Prometheus: bind to
127.0.0.1, reach it through an SSH tunnel or an authenticated reverse proxy (kb/security/ssh-security-hardening).
Adding the Prometheus Datasource
Via the UI
- Connections βΈ Data sources βΈ Add data source βΈ Prometheus
- URL:
http://prometheus:9090β service name on the shared Compose network, notlocalhost - Leave auth empty; set Scrape interval matching your Prometheus (
15s) - Save & test β green "Successfully queried the Prometheus API"
If Save & test fails here, 90% of later panel errors trace back to this one URL.
Verify with a quick query
Open Explore, pick the datasource, type up, press Run. You should see three series (prometheus itself, node-exporter, cadvisor) all with Value = 1. This confirms the whole pipeline: Grafana β Prometheus β exporters.
Pre-Built Dashboards
Before building anything yourself, check grafana.com/grafana/dashboards β thousands of community dashboards import in seconds.
Dashboard βΈ New βΈ Import, enter an ID:
| ID | Dashboard | Needs |
|---|---|---|
| 1860 | Node Exporter Full | node-exporter job |
| 14282 / 193 | Docker/cAdvisor containers | cAdvisor |
| 315 | Prometheus overview | prometheus self-scrape |
| 12019 | Node Exporter (simpler) | node-exporter |
On import:
- Choose a name/UID.
- Select your Prometheus datasource in the dropdown (community dashboards assume one exists).
- If the dashboard uses variables like
$jobor$node, confirm they resolve β Node Exporter Full auto-populates them from label values.
Node Exporter Full alone replaces most of what you'd hand-build: CPU per core, memory breakdown, disk I/O and utilization, filesystem forecasts, network throughput, systemd units. Start there; build customs only for gaps.
π Imported dashboards are read-only JSON copies. Editing them locally is fine β they won't receive upstream updates afterward. Re-import to refresh and diff.
Building Custom Panels
Edit βΈ Add visualization βΈ select Prometheus datasource. Every panel has three parts: query, legend, and visualization type.
Panel types that matter
| Type | Use for | Good example queries |
|---|---|---|
| Time series | anything trending | rate(node_network_receive_bytes_total[5m]) |
| Stat | single current number | count(up == 0) β "targets down" |
| Gauge | % against known max | RAM used % |
| Table | inventory/rankings | topk(10, container_memory_usage_bytes{name!=""}) |
Example: "RAM Used %" gauge
100 * (1 - node_memory_MemAvailable_bytes{instance="$host"}
/ node_memory_MemTotal_bytes{instance="$host"})
- Unit: Percent (0-100) under Standard options β never let raw bytes masquerade as percentages
- Thresholds: green < 70, yellow 70β85, red > 85
Example: top memory consumers table
topk(10,
sum by (name) (container_memory_usage_bytes{name!=""})
)
Set Instant query mode (not range) so tables show current values instead of one row per timestamp.
Panel hygiene
- Name panels after questions, not metrics: "Are we about to run out of disk?" not "node_filesystem_avail".
- Put
rate()windows in variables ($__rate_interval) so they adapt to zoom level. - One idea per panel β a wall of 12-line PromQL in one panel helps nobody at 3 AM.
Variables & Templating
Variables turn one dashboard into N. The classic pattern: a host dropdown feeding every query.
Dashboard settings βΈ Variables βΈ New:
| Field | Value |
|---|---|
| Name | host |
| Type | Query |
| Query (datasource: Prometheus) | label_values(up{job="node"}, instance) |
| Multi-value | β |
| Include All option | β |
Then reference $host anywhere a label value goes:
rate(node_cpu_seconds_total{instance=~"$host",mode="idle"}[$__rate_interval])
Useful variable flavors:
# list containers from cAdvisor data
label_values(container_last_seen{name!=""}, name)
# static list (env selector)
Type: Custom β prod , staging , lab
# nested: services filtered by chosen $host
label_values(process_cpu_seconds_total{instance="$host"}, group)
$__rate_interval is a magic variable that picks a rate window β₯ 4Γ scrape interval at the current zoom β use it everywhere instead of hardcoding [5m].
Chained variables (query A's result filters query B) are how you get prodβregionβhost drill-downs without duplicating dashboards per environment.
Alerting in Grafana
Grafana's unified alerting evaluates any panel-style query on a schedule and routes notifications. Flow: Alerting βΈ Alert rules βΈ New alert rule.
Rule anatomy (example: host down)
- Query A:
up{job="node"}(last value) - Expression: Reduce βΈ Last, then Threshold βΈ below
1 - Evaluation: every
1m, for3m(pending period avoids flap on one bad scrape) - Labels:
severity=critical,team=infra - Annotations: summary
Host {{ $labels.instance }} stopped responding to scrapes
Good starter rules, expressed as thresholds over queries:
Disk almost full:
100*(1 - node_filesystem_avail_bytes{mountpoint="/"}
/node_filesystem_size_bytes{mountpoint="/"}) > 85
Memory pressure:
100*(1 - node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes) > 90
Container restart loop (cAdvisor):
changes(container_start_time_seconds{name!=""}[30m]) > 3
Any target down:
up == 0
Contact points & policies
- Contact points: add Discord/Slack/email integration (webhook URL), then Test it before trusting it β same doctrine as netdata-basics' alarm test and simple-alerts.
- Notification policies: route by label β e.g.
severity=criticalβ pager channel; everything else β daily-digest channel. Default policy catches the rest. - Silences exist for maintenance windows; use them instead of disabling rules.
Anti-fatigue checklist (mirrors simple-alerts):
- Alert on symptoms users feel (disk full in 4 h), not causes (CPU busy)
- Every alert must have an action you'd actually take
- Pending periods β₯ noise duration; review monthly and delete alerts that never mattered
Provisioning as Code
Everything configured above lives in Grafana's SQLite DB by default β lost on rebuild, unversionable, unreproducible. Provisioning fixes this: Grafana loads YAML/JSON from /etc/grafana/provisioning at startup (the Compose file already mounts it). Per Grafana's docs, config files support env-var interpolation ($ENV_VAR_NAME), and each startup reconciles files into the DB.
Directory layout
provisioning/
βββ datasources/
β βββ prometheus.yaml
βββ dashboards/
β βββ provider.yaml
βββ dashboards-json/
βββ host-overview.json
βββ container-overview.json
provisioning/datasources/prometheus.yaml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: prom-main # stable UID β dashboards reference this
access: proxy # server-side requests, not browser
url: http://prometheus:9090
isDefault: true
jsonData:
timeInterval: 15s # matches scrape_interval
Datasources listed here are created/updated on every start β no clicking, no drift between environments.
provisioning/dashboards/provider.yaml
apiVersion: 1
providers:
- name: default
orgId: 1
folder: Infrastructure
type: file
disableDeletion: false
updateIntervalSeconds: 30
options:
path: /etc/grafana/provisioning/dashboards-json
foldersFromFilesStructure: false
Dashboard JSON
Export any dashboard (Share βΈ Export βΈ Save to file) and drop the JSON into dashboards-json/. Two edits make it portable:
- Replace
"datasource": {"type":"prometheus","uid":"OLD-ID"}with"uid": "prom-main"everywhere. - Keep variables intact β they export automatically.
Restart once (docker compose restart grafana) and both datasource and dashboards appear without touching the UI. From here the workflow is Git-native:
git add provisioning/ && git commit -m "grafana: add container overview board"
Rebuild the host β git clone β docker compose up -d β identical Grafana. That's the whole point of Phase 3 done right. (Kubernetes deployments use the same YAML shape via ConfigMaps β see containers/k0s-configmaps-secrets.)
Troubleshooting & Common Pitfalls
"Datasource not found" / panels empty after import
Imported dashboards carry the original author's datasource UID. Re-point each panel (or edit the JSON) to your prom-main UID β the error message names the missing UID verbatim.
Save & test fails against Prometheus
- URL must be reachable from the Grafana container:
http://prometheus:9090(same Compose network), notlocalhost. - Different Compose projects? Attach both to one external network:
networks: { monitoring: { external: true } }.
Panels show "No data" but Explore works
Variable regex mismatch: $host values come from instance labels β if your job renamed targets, the dropdown is stale. Check Dashboard settings βΈ Variables βΈ run the preview query.
Permission denied writing ./data
Grafana runs as uid 472: sudo chown -R 472:472 data. Symptom: crash loop with "migration failed" or lock errors.
Provisioned dashboard won't update
Providers cache by mtime; updateIntervalSeconds defaults can lag. Force with docker compose restart grafana, and remember: UI edits to provisioned dashboards are overwritten by files on next load β edit the JSON, not the UI.
Admin password reset
Provisioning env vars apply on first DB init only. Later resets: docker exec -it grafana grafana cli admin reset-admin-password <newpass>.
Too many series / slow dashboards
A dashboard rendering 50 heavy rate() panels will hammer Prometheus. Mitigate: smaller time ranges, fewer panels per board, recording rules in Prometheus for expensive recurring queries (out of scope here β see Prometheus docs' Recording Rules section).
Sources & Related
Web sources consulted during research (2026-08-26):
- Grafana provisioning documentation (datasources, providers, env vars): https://grafana.com/docs/grafana/latest/administration/provisioning/
- Grafana dashboards gallery (import IDs): https://grafana.com/grafana/dashboards/
- Prometheus docs β Visualizing metrics using Grafana: https://prometheus.io/docs/tutorials/visualizing_metrics_using_grafana/
- Node Exporter Full dashboard (ID 1860): https://grafana.com/grafana/dashboards/1860-node-exporter-full/
Related KB lessons:
- Previous phase: prometheus-basics β where these metrics come from
- Also Phase 1: netdata-basics, uptime-kuma-setup
- Alert design doctrine: simple-alerts
- Compose networking between stacks: containers/docker-networking, containers/docker-compose-patterns
- Same provisioning pattern on Kubernetes: containers/k0s-configmaps-secrets
Change Log
- 2026-08-26: Initial draft created via headless-browser web research session.