Grafana Dashboards: Building Effective Visualizations
Create beautiful and useful Grafana dashboards. Best practices and example dashboards.
Grafana is the industry-standard visualization platform for monitoring data. This comprehensive guide will teach you how to build effective, beautiful dashboards that provide actionable insights into your homelab infrastructure.
What You’ll Learn
- Dashboard design principles and best practices
- Creating panels with PromQL queries
- Using variables for dynamic dashboards
- Building reusable dashboard templates
- Alerting integration and annotations
- Performance optimization for low-resource systems
- Community dashboard import and customization
Prerequisites
- Grafana 10.x or later installed
- Prometheus or compatible data source
- Basic understanding of metrics and time series
- Access to your Grafana instance
Why Custom Dashboards Matter
While Grafana provides thousands of community dashboards, custom dashboards tailored to your specific infrastructure provide:
- Relevant Metrics: Only show what matters to your setup
- Faster Troubleshooting: Quick identification of issues
- Resource Efficiency: Optimized queries reduce load
- Better Context: Organize metrics logically for your workflow
- Learning Opportunity: Deep understanding of your stack
Dashboard Design Principles
The 5-Second Rule
Your dashboard should answer critical questions within 5 seconds:
- Is everything running?
- Are there any alerts?
- What’s the current resource usage?
- Are there any trends to worry about?
Visual Hierarchy
Organize panels from most to least important:
- Status Row: Overall health, alert counts
- Critical Metrics: CPU, memory, disk, network
- Application Metrics: Service-specific data
- Detailed Metrics: Deep-dive panels
- Logs/Traces: Troubleshooting information
Color Conventions
Use consistent color schemes:
- Green: Healthy, normal operation
- Yellow: Warning, attention needed
- Red: Critical, immediate action required
- Blue: Informational, neutral metrics
- Purple: Custom/special metrics
Setting Up Prometheus Data Source
First, ensure your Prometheus data source is configured properly.
Docker Compose for Grafana with Prometheus
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./prometheus/config:/etc/prometheus
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle'
- '--web.enable-admin-api'
networks:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=YourSecurePassword123
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-piechart-panel,grafana-worldmap-panel
- GF_SERVER_ROOT_URL=http://YOUR-SERVER-IP:3000
- GF_ANALYTICS_REPORTING_ENABLED=false
- GF_ANALYTICS_CHECK_FOR_UPDATES=false
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
depends_on:
- prometheus
networks:
- monitoring
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
ports:
- "9100:9100"
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
networks:
- monitoring
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
privileged: true
devices:
- /dev/kmsg
networks:
- monitoring
volumes:
prometheus-data:
grafana-data:
networks:
monitoring:
driver: bridge Prometheus Configuration
Create ./prometheus/config/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: 'homelab'
environment: 'production'
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
labels:
instance: 'homelab-server'
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
- job_name: 'docker-containers'
static_configs:
- targets: ['YOUR-SERVER-IP:9323'] Deploy the stack:
docker-compose up -d Access Grafana at http://YOUR-SERVER-IP:3000 (default credentials: admin/YourSecurePassword123).
Provisioning Data Sources
Automate data source configuration with provisioning.
Create ./grafana/provisioning/datasources/prometheus.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
jsonData:
timeInterval: 15s
queryTimeout: 60s
httpMethod: POST
version: 1 This automatically adds Prometheus when Grafana starts.
Building Your First Dashboard
Dashboard Structure
A well-organized dashboard follows this structure:
{
"dashboard": {
"title": "Homelab Overview",
"tags": ["homelab", "overview"],
"timezone": "browser",
"refresh": "30s",
"time": {
"from": "now-6h",
"to": "now"
},
"panels": []
}
} Creating a System Overview Dashboard
Let’s build a comprehensive system overview dashboard step by step.
1. Status Row Panel
Create a row to organize related panels:
Panel Configuration:
- Type: Row
- Title: System Status
- Collapsed: false
2. Uptime Stat Panel
Query (PromQL):
time() - node_boot_time_seconds{instance="homelab-server"} Panel Configuration:
- Type: Stat
- Title: System Uptime
- Unit: Seconds (s)
- Decimals: 0
- Thresholds: None needed
- Value Mappings: None
- Display: Calculate → Last (not null)
Additional Options:
- Color Mode: Background
- Graph Mode: None
- Text Size: Title (20), Value (50)
3. CPU Usage Gauge
Query (PromQL):
100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) Panel Configuration:
- Type: Gauge
- Title: CPU Usage
- Unit: Percent (0-100)
- Min: 0
- Max: 100
- Thresholds:
- Base: Green
- 70: Yellow
- 85: Red
- Show threshold markers: Yes
4. Memory Usage Gauge
Query (PromQL):
100 * (1 - ((node_memory_MemAvailable_bytes{instance="homelab-server"}) / (node_memory_MemTotal_bytes{instance="homelab-server"}))) Panel Configuration:
- Type: Gauge
- Title: Memory Usage
- Unit: Percent (0-100)
- Thresholds:
- Base: Green
- 80: Yellow
- 90: Red
5. Disk Usage Table
Query (PromQL):
100 - ((node_filesystem_avail_bytes{mountpoint="/",fstype!="rootfs"} * 100) / node_filesystem_size_bytes{mountpoint="/",fstype!="rootfs"}) Panel Configuration:
- Type: Table
- Title: Disk Usage
- Columns:
- Mountpoint
- Used %
- Total Size
- Available
Transform: Format table with proper units
6. Network Traffic Graph
Queries (PromQL):
Receive (Query A):
irate(node_network_receive_bytes_total{device!~"lo|docker.*|veth.*"}[5m]) Transmit (Query B):
irate(node_network_transmit_bytes_total{device!~"lo|docker.*|veth.*"}[5m]) Panel Configuration:
- Type: Time series
- Title: Network Traffic
- Unit: Bytes/sec (Bps)
- Legend: Show → Bottom
- Fill Opacity: 10
- Line Width: 2
- Point Size: 5
- Stacking: None
- Gradient Mode: Opacity
Complete Dashboard JSON Example
Here’s a production-ready dashboard JSON for homelab monitoring:
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"panels": [],
"title": "System Overview",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "s"
}
},
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 1
},
"id": 2,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "time() - node_boot_time_seconds",
"refId": "A"
}
],
"title": "Uptime",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 85
}
]
},
"unit": "percent"
}
},
"gridPos": {
"h": 4,
"w": 6,
"x": 6,
"y": 1
},
"id": 3,
"options": {
"orientation": "auto",
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "100 - (avg(irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)",
"refId": "A"
}
],
"title": "CPU Usage",
"type": "gauge"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 80
},
{
"color": "red",
"value": 90
}
]
},
"unit": "percent"
}
},
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 1
},
"id": 4,
"options": {
"orientation": "auto",
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes))",
"refId": "A"
}
],
"title": "Memory Usage",
"type": "gauge"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 80
},
{
"color": "red",
"value": 90
}
]
},
"unit": "percent"
}
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 1
},
"id": 5,
"options": {
"orientation": "auto",
"reduceOptions": {
"values": false,
"calcs": ["lastNotNull"],
"fields": ""
},
"showThresholdLabels": false,
"showThresholdMarkers": true
},
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "100 - ((node_filesystem_avail_bytes{mountpoint="/"} * 100) / node_filesystem_size_bytes{mountpoint="/"})",
"refId": "A"
}
],
"title": "Disk Usage",
"type": "gauge"
}
],
"refresh": "30s",
"schemaVersion": 38,
"style": "dark",
"tags": ["homelab", "overview"],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Homelab System Overview",
"uid": "homelab-overview",
"version": 1
} To import this dashboard:
- Go to Dashboards → Import
- Paste the JSON
- Select your Prometheus data source
- Click Import
Using Variables for Dynamic Dashboards
Variables make dashboards flexible and reusable across multiple hosts.
Creating Variables
Navigate to Dashboard Settings → Variables → Add variable.
Instance Variable
Name: instance
Type: Query
Data source: Prometheus
Query: label_values(node_uname_info, instance)
Refresh: On Dashboard Load
Multi-value: Yes
Include All option: Yes Job Variable
Name: job
Type: Query
Data source: Prometheus
Query: label_values(up, job)
Refresh: On Dashboard Load
Multi-value: Yes
Include All option: Yes Interface Variable
Name: device
Type: Query
Data source: Prometheus
Query: label_values(node_network_receive_bytes_total{instance="$instance"}, device)
Regex: ^(?!veth|docker|lo).*
Refresh: On Time Range Change
Multi-value: Yes
Include All option: Yes Using Variables in Queries
Update your queries to use variables:
CPU Usage with Instance Variable:
100 - (avg by (instance) (irate(node_cpu_seconds_total{instance=~"$instance", mode="idle"}[5m])) * 100) Network Traffic with Device Variable:
irate(node_network_receive_bytes_total{instance=~"$instance", device=~"$device"}[5m]) Container CPU by Job:
rate(container_cpu_usage_seconds_total{job=~"$job", name!=""}[5m]) * 100 Advanced Panel Types
Heatmap Panel
Perfect for visualizing latency distributions.
Query:
sum(rate(http_request_duration_seconds_bucket[5m])) by (le) Configuration:
- Format: Heatmap
- Color Scheme: Spectral
- Data Format: Time series buckets
Bar Gauge Panel
Great for comparing multiple metrics.
Query:
topk(10, container_memory_usage_bytes{name!=""}) Configuration:
- Orientation: Horizontal
- Display Mode: Gradient
- Show Values: Always
State Timeline Panel
Visualize service status over time.
Query:
up{job="node-exporter"} Configuration:
- Value mappings:
- 0 → Down (Red)
- 1 → Up (Green)
- Fill opacity: 70%
Dashboard Templating
Repeating Panels
Create a single panel that repeats for each variable value.
Example: Per-Container CPU Usage
- Create a variable for container names:
Name: container
Type: Query
Query: label_values(container_cpu_usage_seconds_total, name) - Create a panel with this query:
rate(container_cpu_usage_seconds_total{name="$container"}[5m]) * 100 - In panel settings:
- Repeat options → Repeat by variable: container
- Repeat direction: Horizontal
Row Repeating
Repeat entire rows for different environments or hosts.
Example: Per-Host Monitoring
- Create a host variable
- Create a row with multiple panels
- Set row to repeat by host variable
- All panels in the row will repeat for each host
Alerting Integration
Adding Alert Rules to Panels
Configure alerts directly in your dashboard panels.
High CPU Alert:
- Edit your CPU panel
- Go to Alert tab → Create alert rule
- Configure:
Rule name: High CPU Usage
Evaluate every: 1m
For: 5m
Conditions:
- WHEN avg() OF query(A, 5m, now) IS ABOVE 85
Annotations:
summary: CPU usage is above 85%
description: Instance {{ $labels.instance }} has CPU usage of {{ $value }}% Alert Notification Channels
Configure notification channels for alerts.
Discord Webhook:
Type: Discord
Webhook URL: https://discord.com/api/webhooks/YOUR-WEBHOOK-ID/YOUR-WEBHOOK-TOKEN
Message: {{ template "discord.message" . }} Slack Webhook:
Type: Slack
Webhook URL: https://hooks.slack.com/services/YOUR/WEBHOOK/URL
Username: Grafana
Recipient: #monitoring Performance Optimization
Query Optimization
Bad Query (Slow):
sum(rate(container_cpu_usage_seconds_total[5m])) by (name) Good Query (Fast):
sum(rate(container_cpu_usage_seconds_total{name!=""}[5m])) by (name) Always filter out empty labels to reduce cardinality.
Dashboard Performance
- Limit Time Range: Don’t default to 30d
- Reduce Panel Count: Maximum 20-30 panels per dashboard
- Increase Refresh Interval: 30s minimum for homelab
- Use Recording Rules: Pre-calculate expensive queries
- Avoid Wildcards: Be specific with label matchers
Recording Rules for Performance
Create ./prometheus/config/rules.yml:
groups:
- name: homelab_aggregations
interval: 30s
rules:
- record: instance:cpu_usage:rate5m
expr: 100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
- record: instance:memory_usage:ratio
expr: 1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
- record: instance:disk_usage:ratio
expr: 1 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})
- record: instance:network_receive:rate5m
expr: sum(rate(node_network_receive_bytes_total{device!~"lo|docker.*|veth.*"}[5m])) by (instance)
- record: instance:network_transmit:rate5m
expr: sum(rate(node_network_transmit_bytes_total{device!~"lo|docker.*|veth.*"}[5m])) by (instance) Update prometheus.yml to include rules:
rule_files:
- 'rules.yml' Use recording rules in queries:
instance:cpu_usage:rate5m{instance=~"$instance"} Community Dashboards
Importing Community Dashboards
- Browse https://grafana.com/grafana/dashboards/
- Find a dashboard (e.g., Node Exporter Full - ID: 1860)
- In Grafana: Dashboards → Import → Enter ID: 1860
- Select data source → Import
Recommended Dashboards
- Node Exporter Full (1860): Comprehensive host metrics
- Docker Container & Host Metrics (10619): Container monitoring
- Prometheus 2.0 Stats (3662): Prometheus internals
- NGINX Ingress Controller (9614): Nginx metrics
- PostgreSQL Database (9628): Database monitoring
Customizing Imported Dashboards
- Save imported dashboard with new name
- Remove unnecessary panels
- Add your custom variables
- Adjust thresholds for your environment
- Export and version control the JSON
Low-Resource Optimization
For Raspberry Pi or low-power homelabs:
Grafana Configuration
Create ./grafana/grafana.ini:
[server]
protocol = http
http_port = 3000
root_url = http://YOUR-SERVER-IP:3000
[database]
type = sqlite3
path = grafana.db
[analytics]
reporting_enabled = false
check_for_updates = false
[dashboards]
versions_to_keep = 5
min_refresh_interval = 30s
[users]
allow_sign_up = false
auto_assign_org = true
auto_assign_org_role = Viewer
[auth.anonymous]
enabled = false
[security]
disable_gravatar = true
[snapshots]
external_enabled = false
[alerting]
enabled = true
execute_alerts = true
[metrics]
enabled = false
[explore]
enabled = true Mount this in docker-compose:
volumes:
- ./grafana/grafana.ini:/etc/grafana/grafana.ini:ro Resource Limits
Add to your Grafana service:
deploy:
resources:
limits:
cpus: '1'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M Backup and Version Control
Exporting Dashboards
Export individual dashboards:
- Dashboard Settings → JSON Model
- Copy JSON
- Save to file:
dashboards/homelab-overview.json
Automated Backup Script
Create backup-dashboards.sh:
#!/bin/bash
GRAFANA_URL="http://YOUR-SERVER-IP:3000"
GRAFANA_API_KEY="your-api-key"
BACKUP_DIR="./dashboards/backup"
mkdir -p "$BACKUP_DIR"
# Get all dashboard UIDs
DASHBOARDS=$(curl -s -H "Authorization: Bearer $GRAFANA_API_KEY" "$GRAFANA_URL/api/search?type=dash-db" | jq -r '.[].uid')
# Export each dashboard
for uid in $DASHBOARDS; do
echo "Backing up dashboard: $uid"
curl -s -H "Authorization: Bearer $GRAFANA_API_KEY" "$GRAFANA_URL/api/dashboards/uid/$uid" | jq . > "$BACKUP_DIR/$uid.json"
done
echo "Backup completed: $(date)" Create API key in Grafana: Configuration → API Keys → Add API key (Viewer role).
Git Version Control
git init
git add dashboards/
git commit -m "Initial dashboard backup"
git remote add origin YOUR-GIT-REPO
git push -u origin main Troubleshooting
No Data in Panels
Check Prometheus targets:
curl http://YOUR-SERVER-IP:9090/api/v1/targets All targets should show state: “up”.
Verify metrics exist:
curl 'http://YOUR-SERVER-IP:9090/api/v1/query?query=up' Check Grafana data source:
- Configuration → Data Sources → Prometheus
- Click “Save & Test” → Should show “Data source is working”
Slow Dashboard Loading
- Reduce time range: Use last 6h instead of 7d
- Limit queries: Remove or disable unused panels
- Increase scrape interval: Change from 15s to 30s
- Use recording rules: Pre-calculate complex queries
- Check Prometheus memory: May need more RAM
Variables Not Working
- Verify variable query syntax: Test in Prometheus
- Check refresh settings: Set to “On Dashboard Load”
- Validate regex patterns: Test with regex101.com
- Review dependencies: Ensure variables are in correct order
Best Practices Summary
- Organization: Use rows to group related panels
- Naming: Clear, descriptive panel titles
- Colors: Consistent color scheme across dashboards
- Variables: Make dashboards reusable
- Alerts: Add alerts to critical metrics
- Documentation: Add text panels with context
- Performance: Optimize queries, limit panels
- Backup: Version control dashboard JSON
- Testing: Verify queries in Prometheus first
- Simplicity: Start simple, add complexity as needed
Next Steps
- Explore Grafana Loki for log aggregation
- Set up Prometheus Alertmanager for advanced alerting
- Create custom plugins for specialized visualizations
- Implement distributed tracing with Tempo
- Build application-specific dashboards
- Set up multi-tenant dashboards for family members
Additional Resources
- Grafana Documentation: https://grafana.com/docs/
- PromQL Cheat Sheet: https://promlabs.com/promql-cheat-sheet/
- Community Dashboards: https://grafana.com/grafana/dashboards/
- Prometheus Best Practices: https://prometheus.io/docs/practices/
With these skills, you can create powerful, beautiful dashboards that provide deep insights into your homelab infrastructure. Start with the basics and gradually add more sophisticated visualizations as you become comfortable with Grafana and PromQL.