Monitoring intermediate ⏱ 60 minutes

Grafana Dashboards: Building Effective Visualizations

Create beautiful and useful Grafana dashboards. Best practices and example dashboards.

By homelabbing.io • 2025-12-17
#grafana#dashboards#visualization#prometheus#monitoring

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:

  1. Status Row: Overall health, alert counts
  2. Critical Metrics: CPU, memory, disk, network
  3. Application Metrics: Service-specific data
  4. Detailed Metrics: Deep-dive panels
  5. 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:

  1. Go to Dashboards → Import
  2. Paste the JSON
  3. Select your Prometheus data source
  4. 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

  1. Create a variable for container names:
Name: container
Type: Query
Query: label_values(container_cpu_usage_seconds_total, name)
  1. Create a panel with this query:
rate(container_cpu_usage_seconds_total{name="$container"}[5m]) * 100
  1. 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

  1. Create a host variable
  2. Create a row with multiple panels
  3. Set row to repeat by host variable
  4. 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:

  1. Edit your CPU panel
  2. Go to Alert tab → Create alert rule
  3. 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

  1. Limit Time Range: Don’t default to 30d
  2. Reduce Panel Count: Maximum 20-30 panels per dashboard
  3. Increase Refresh Interval: 30s minimum for homelab
  4. Use Recording Rules: Pre-calculate expensive queries
  5. 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

  1. Browse https://grafana.com/grafana/dashboards/
  2. Find a dashboard (e.g., Node Exporter Full - ID: 1860)
  3. In Grafana: Dashboards → Import → Enter ID: 1860
  4. Select data source → Import
  • 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

  1. Save imported dashboard with new name
  2. Remove unnecessary panels
  3. Add your custom variables
  4. Adjust thresholds for your environment
  5. 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:

  1. Dashboard Settings → JSON Model
  2. Copy JSON
  3. 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

  1. Reduce time range: Use last 6h instead of 7d
  2. Limit queries: Remove or disable unused panels
  3. Increase scrape interval: Change from 15s to 30s
  4. Use recording rules: Pre-calculate complex queries
  5. Check Prometheus memory: May need more RAM

Variables Not Working

  1. Verify variable query syntax: Test in Prometheus
  2. Check refresh settings: Set to “On Dashboard Load”
  3. Validate regex patterns: Test with regex101.com
  4. Review dependencies: Ensure variables are in correct order

Best Practices Summary

  1. Organization: Use rows to group related panels
  2. Naming: Clear, descriptive panel titles
  3. Colors: Consistent color scheme across dashboards
  4. Variables: Make dashboards reusable
  5. Alerts: Add alerts to critical metrics
  6. Documentation: Add text panels with context
  7. Performance: Optimize queries, limit panels
  8. Backup: Version control dashboard JSON
  9. Testing: Verify queries in Prometheus first
  10. 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


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.