Building a Home Assistant Homelab Dashboard¶
A homelab dashboard should answer a few useful questions quickly:
- Are my important services reachable?
- Is a host running out of CPU, memory or disk?
- Are my containers doing anything strange?
- Is the network itself healthy?
- Did a backup fail?
Home Assistant is a good place to present those answers because it is fast to open, works well on a wall tablet, and is already where many of us look for alerts. It is not the best place to store every metric from every server. Let Prometheus keep the detailed telemetry and history. Let Home Assistant show the small set of things worth acting on.
This is the pattern behind the Techdox homelab dashboard: exporters and probes collect the data, Prometheus stores it, and Home Assistant imports a curated summary for a simple control-room view.
graph LR
A[Linux hosts] -->|node_exporter| P[Prometheus]
B[Docker hosts] -->|cAdvisor / Docker metrics| P
C[Services and websites] -->|Blackbox Exporter| P
D[Network devices] -->|Exporter or API| P
P -->|small REST queries| H[Home Assistant]
P --> G[Grafana]
H --> L[Native Lovelace dashboard] What you need before starting¶
- A working Home Assistant installation.
- A private Prometheus instance. See the Prometheus guide if you do not have one yet.
- Node Exporter on each Linux host you want to track.
- A way to monitor user-facing services, such as Blackbox Exporter or Uptime Kuma.
- A basic understanding of your network names and which services are actually important.
Note
Start with three hosts and five important services. A dashboard that tells you the truth is better than a huge wall of cards that nobody reads.
Decide what belongs on the dashboard¶
Use Home Assistant for the things you want to see at a glance or receive a notification about:
| Area | Good dashboard signals | Better kept in Prometheus/Grafana |
|---|---|---|
| Hosts | CPU, memory, root disk, online/offline | Per-process or long-term capacity analysis |
| Containers | Container host reachable, key application healthy, unusual resource usage | Every container metric and long historical graphs |
| Services | HTTPS probe success, response time, certificate warning | Full latency distributions and request rates |
| Network | DNS resolver online, WAN/router health, active clients | Packet-level troubleshooting and flow data |
| Platforms | Proxmox guest status, Kubernetes node readiness, backup outcome | Full cluster and hypervisor diagnostics |
A container being running does not prove its application works. Monitor both layers:
- Container/host health tells you whether Docker and the underlying machine are alive.
- Service probes tell you whether users can actually reach the application.
A green container with a broken reverse proxy is still a broken service. Computers are very good at being technically alive while entirely useless.
Collect the metrics¶
Linux hosts with Node Exporter¶
Install Node Exporter on every Linux host you want to include. It exposes CPU, memory, filesystem and network-interface metrics on port 9100.
Use predictable Prometheus job names so queries stay readable:
scrape_configs:
- job_name: node_exporter
static_configs:
- targets:
- server-one.example.internal:9100
- server-two.example.internal:9100
- docker-host.example.internal:9100
See Setting Up Node Exporter for the host-side service setup.
Warning
Node Exporter can reveal useful information about your hosts. Keep port 9100 on your trusted LAN or a private monitoring network. Do not publish it directly to the internet.
Docker and container resources¶
For Docker hosts, add cAdvisor to expose per-container CPU, memory, filesystem and network metrics. It should only be reachable by Prometheus.
services:
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
container_name: cadvisor
restart: unless-stopped
privileged: true
ports:
- "127.0.0.1:8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker:/var/lib/docker:ro
If Prometheus runs on another host, do not bind cAdvisor only to 127.0.0.1. Instead, bind it to a private interface and firewall port 8080 so only the Prometheus host can connect.
Add it to Prometheus:
Warning
cAdvisor needs read access to Docker and host filesystem metadata. Keep it private and do not place it behind a public reverse proxy.
Service health with Blackbox Exporter¶
Use HTTP probes for the services people actually use. This catches failures that a running container cannot: DNS, TLS, reverse proxy, login page and application startup problems.
scrape_configs:
- job_name: blackbox_http
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://status.example.com
- https://photos.example.com
- https://requests.example.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter.example.internal:9115
For internal-only services, make sure the Prometheus host resolves the same private DNS names that your users do. Do not create public DNS records just to satisfy a monitor.
Network and platform signals¶
Add these once the basics are stable:
- Pi-hole or AdGuard Home: queries, blocked percentage, active clients and resolver status.
- OPNsense/pfSense: WAN state, gateway loss/latency, interface traffic and DHCP leases through a read-only exporter/API account.
- Proxmox: node resources, guest running state and storage usage with a read-only audit token.
- Kubernetes: node readiness, unavailable deployments and problem pods through kube-state-metrics.
- Backups: last successful backup time and whether the last attempt failed.
Use a dedicated read-only identity for any router, hypervisor or API integration. Test that it can read the data you need but cannot restart guests, change firewall rules or write configuration.
Add a small Prometheus bridge to Home Assistant¶
Home Assistant can query Prometheus through its REST integration. Keep the requests small and intentional. The example below creates six simple dashboard entities, refreshed every minute.
Create a dedicated Home Assistant-to-Prometheus path on your private network, then add this to configuration.yaml:
rest:
- resource: "http://prometheus.example.internal:9090/api/v1/query"
params:
query: 'count(up{job=~"node_exporter|blackbox_http"})'
scan_interval: 60
sensor:
- name: Homelab Prometheus Targets Total
unique_id: homelab_prometheus_targets_total
value_template: "{{ value_json.data.result[0].value[1] | int(0) }}"
- resource: "http://prometheus.example.internal:9090/api/v1/query"
params:
query: 'sum(up{job=~"node_exporter|blackbox_http"})'
scan_interval: 60
sensor:
- name: Homelab Prometheus Targets Up
unique_id: homelab_prometheus_targets_up
value_template: "{{ value_json.data.result[0].value[1] | int(0) }}"
- resource: "http://prometheus.example.internal:9090/api/v1/query"
params:
query: 'count(probe_success{job="blackbox_http"})'
scan_interval: 60
sensor:
- name: Homelab Services Total
unique_id: homelab_services_total
value_template: "{{ value_json.data.result[0].value[1] | int(0) }}"
- resource: "http://prometheus.example.internal:9090/api/v1/query"
params:
query: 'sum(probe_success{job="blackbox_http"})'
scan_interval: 60
sensor:
- name: Homelab Services Up
unique_id: homelab_services_up
value_template: "{{ value_json.data.result[0].value[1] | int(0) }}"
- resource: "http://prometheus.example.internal:9090/api/v1/query"
params:
query: 'count(ALERTS{alertstate="firing"})'
scan_interval: 60
sensor:
- name: Homelab Active Alerts
unique_id: homelab_active_alerts
value_template: "{{ value_json.data.result[0].value[1] | int(0) }}"
- resource: "http://prometheus.example.internal:9090/api/v1/query"
params:
query: 'avg(100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100))'
scan_interval: 60
sensor:
- name: Homelab Average CPU
unique_id: homelab_average_cpu
unit_of_measurement: "%"
value_template: "{{ value_json.data.result[0].value[1] | float(0) | round(1) }}"
Replace the job names with the names in your own Prometheus configuration. Test every query in the Prometheus expression browser first. If a query returns no result, Home Assistant should show unknown, not a reassuring but false zero.
Warning
Prometheus has no built-in authentication by default. Keep it private, restrict the firewall so only trusted hosts can query it, and use a proxy or authentication layer if you need access outside your management network. Never put a long-lived API token directly in a dashboard YAML file. Store secrets in secrets.yaml.
Add a status sensor¶
Add this below the REST configuration to turn the totals into a clear dashboard state:
template:
- sensor:
- name: Homelab Health
unique_id: homelab_health
icon: mdi:home-analytics
state: >
{% set targets_total = states('sensor.homelab_prometheus_targets_total') | int(-1) %}
{% set targets_up = states('sensor.homelab_prometheus_targets_up') | int(-1) %}
{% set services_total = states('sensor.homelab_services_total') | int(-1) %}
{% set services_up = states('sensor.homelab_services_up') | int(-1) %}
{% set alerts = states('sensor.homelab_active_alerts') | int(-1) %}
{% if -1 in [targets_total, targets_up, services_total, services_up, alerts] %}
unknown
{% elif alerts > 0 or targets_up < targets_total or services_up < services_total %}
degraded
{% else %}
healthy
{% endif %}
This deliberately treats unavailable source data as unknown. A monitoring dashboard that silently turns missing data into healthy is worse than no dashboard at all.
Validate the configuration before restarting¶
In Home Assistant, go to Developer Tools โ YAML, check the configuration, then restart Home Assistant. Do not restart blindly after pasting a large configuration block.
After it comes back:
- Open Developer Tools โ States.
- Confirm all six
sensor.homelab_*REST sensors have real values. - Confirm
sensor.homelab_healthishealthy,degradedorunknownfor the right reason. - Compare one or two values with the Prometheus expression browser.
- If anything is missing, fix the Prometheus query before creating dashboard cards.
Build the native Home Assistant dashboard¶
Create a new dashboard, then add a manual card using the built-in cards below. This is intentionally plain Home Assistant. It works without HACS and is easy to maintain when the frontend changes.
type: vertical-stack
cards:
- type: markdown
content: >-
## Homelab control room
{{ states('sensor.homelab_health') | capitalize }} ยท
{{ states('sensor.homelab_active_alerts') }} active alerts
- type: glance
title: At a glance
state_color: true
entities:
- entity: sensor.homelab_health
name: Overall health
- entity: sensor.homelab_prometheus_targets_up
name: Targets up
- entity: sensor.homelab_services_up
name: Services up
- entity: sensor.homelab_active_alerts
name: Alerts
- type: entities
title: Monitoring coverage
entities:
- entity: sensor.homelab_prometheus_targets_up
name: Targets up
- entity: sensor.homelab_prometheus_targets_total
name: Targets total
- entity: sensor.homelab_services_up
name: Services up
- entity: sensor.homelab_services_total
name: Services total
- entity: sensor.homelab_average_cpu
name: Average host CPU
- type: gauge
entity: sensor.homelab_average_cpu
name: Average host CPU
min: 0
max: 100
severity:
green: 0
yellow: 70
red: 90
Add further cards only when you have a clear decision they support. Useful next additions are:
- A host view with CPU, memory and disk per server.
- A service view with only the applications you would notice being down.
- A network view with DNS, WAN gateway and active client state.
- A platform view for Proxmox guests, Kubernetes nodes and backup outcome.
Container status: what to show¶
Do not make the dashboard a raw list of every container. It gets noisy quickly and does not tell you what needs attention.
A practical container section usually has:
- Docker host online from a Node Exporter or ICMP check.
- Docker daemon/container resource usage from cAdvisor.
- Critical application probe status from Blackbox Exporter or Uptime Kuma.
- A link to Grafana for the detailed view when something is wrong.
For a single critical container, create a specific alert or probe. For example, probe the actual web endpoint of your password manager rather than checking whether its container name appears in Docker output.
Keep the dashboard private¶
A homelab dashboard often exposes hostnames, service names, storage usage and internal topology. Treat it as an admin surface.
- Keep Home Assistant behind LAN or VPN access unless you have a deliberate remote-access design.
- Keep exporter ports and Prometheus private.
- Use read-only credentials for network devices and hypervisors.
- Avoid publishing screenshots that reveal private hostnames, IP addresses, tokens or storage paths.
- Back up Home Assistant before changing YAML or dashboards.
How to verify it is working¶
You are done when all of these are true:
- Prometheus shows the intended exporter and probe targets as
UP. - The REST sensors in Home Assistant match the relevant Prometheus query results.
- An intentionally stopped non-critical test service changes the service count or health state as expected.
- Home Assistant shows
unknownwhen Prometheus is unreachable, rather than a false healthy state. - The dashboard renders properly on both desktop and mobile.
- Grafana still has the full history for when you need to investigate a spike or outage.
Where to go next¶
Once the dashboard is stable, add notifications for real actions: a failed backup, a full disk, a service that has been down for several minutes, or a failed WAN gateway. Avoid sending a notification for every transient scrape failure. Your phone deserves better.
Useful companion guides:
- Setting Up Prometheus with Docker Compose
- Setting Up Node Exporter
- Setting Up Grafana with Docker Compose
- Setting Up cAdvisor with Docker Compose
If there is an issue with this guide or you wish to suggest changes, please raise an issue on GitHub.