all systems self-hosted

~/selfhostjournal $

← All articles

,

Monitoring my Docker host with Home Assistant: containers, disks, HTTP checks and alerts

7–11 minutes

In short

Problem: A container that quietly stops, a disk that fills up, a service that answers with the wrong page: on a home server you usually find out when someone in the family says “it doesn’t work”.

Solution: Home Assistant watches the Docker host the same way it watches the rest of the house: one entity per container, host metrics, HTTP checks from the outside, backup status, and a handful of automations that send a Telegram message when something is wrong.

Tested with: Home Assistant 2026.10, the Portainer integration, my own SSH Linux Management add-on (MQTT), about 25 containers on one Docker host, Telegram and the Companion app for notifications

Home Assistant already knows how to tell me when a window is open or a battery is low. So instead of running a separate monitoring stack for my server, I let Home Assistant watch that too. Everything ends up in the same place: one dashboard, one notification channel, and automations I already know how to write. This article shows the two integrations I use to get the Docker host into Home Assistant, the alerts built on top of them, and what I learned along the way.

1. Two integrations, two jobs

Home Assistant runs in a VM, the containers on the host next to it. To see inside the host, I use two integrations, because each one is good at something different:

Portainer integrationSSH Linux Management (add-on)
Connects viaPortainer’s API with an access tokenOne SSH connection to the host
Per containerStatus, CPU, memory, restart count, out-of-memory, image updates, restart/pause/recreate buttonsRunning on/off as a switch, restart button
How fastPolled at a regular intervalPushed from Docker’s event stream, instantly
Host–CPU, RAM, disk use per mount, libvirt VMs
Extra–Reads the backup status file of my backup setup

For the alerts below I use the Portainer status sensors for containers and the SSH add-on for everything about the host. If you only want one, start with Portainer: it’s a built-in integration and gives you the most per container.

2. The Portainer integration

If Portainer already runs on your Docker host, this is the quickest start:

  1. In Portainer, open your user profile → Access tokens → Add access token, and copy the token.
  2. In Home Assistant: Settings → Devices & services → Add integration → Portainer, enter Portainer’s URL and the token.
  3. Home Assistant creates one device per container, with entities like binary_sensor.<container>_status, sensor.<container>_memory_usage, sensor.<container>_restart_count and update.<container>_image_update_available.

The token can do what its user can do: with an admin’s token, Home Assistant can stop and recreate every container. Create a separate Portainer user for Home Assistant if your edition supports roles, and treat the token like a password: it lives in Home Assistant’s config, so your Home Assistant backups contain it too.

One thing Portainer doesn’t do: when you delete a container, its device stays in Home Assistant as “unavailable”. Remove it by hand (Settings → Devices, the device, Delete), or your dashboards and automations slowly fill with dead entities.

3. The SSH add-on: host metrics without root

For the host itself (disk, CPU, RAM, VMs) I wrote a small Home Assistant add-on, SSH Linux Management. It opens one outbound SSH connection to the host and publishes what it finds through MQTT discovery, so the entities appear in Home Assistant without any YAML:

  • Docker: the remote Docker socket is forwarded through the SSH connection. No Docker TCP port is opened, and container changes arrive instantly through Docker’s event stream.
  • Host metrics: CPU, memory and disk use per mount point (I watch / and the Nextcloud data disk).
  • VMs: libvirt VMs as switches with a reboot button, through virsh over the same connection.
  • The SSH account is not root: it’s a normal user in the docker (and libvirt) group, and the host key is pinned on the first connection.

Be honest about the docker group: anyone who can talk to the Docker socket can start a container that mounts the host’s root filesystem. Membership in that group is effectively root on that machine. “Not root” here means: no root password in Home Assistant, one dedicated key you can revoke, and nothing else that account can log in to. If you want real limits, put a Docker socket proxy in front that only allows the API calls you need.

A lesson from running it: the first versions named each Docker entity after the container’s ID. Every docker compose up after an update creates a new container with a new ID, so Home Assistant got a new entity each time, and after a few weeks I had switch.…_traefik, …_traefik_2 up to …_traefik_5. Version 0.11.0 keys the entities on the container name and removes discovery entries for containers that are gone. If you write your own MQTT discovery: choose the unique_id from something that survives a recreate.

4. Alert: a container stopped

The most important automation is also the simplest: if any container’s status sensor is off for two minutes, send a message that names it. The two minutes keep normal restarts (updates, a recreate) from sending false alarms:

alias: System - Docker - Container Down Alert
description: A watched container is not running for 2 minutes → Telegram + push.
mode: parallel
max: 10
triggers:
  - trigger: state
    entity_id:
      - binary_sensor.traefik_status
      - binary_sensor.crowdsec_status
      - binary_sensor.authentik_server_status
      - binary_sensor.nextcloud_nextcloud_1_status
      - binary_sensor.vaultwarden_status
      - binary_sensor.zigbee2mqtt_status
      # … one line per container you care about
    to: "off"
    for:
      minutes: 2
actions:
  - action: telegram_bot.send_message
    data:
      title: "🔴 Docker container down"
      message: "{{ trigger.to_state.attributes.friendly_name }} has not been running for 2 minutes."
  - action: notify.mobile_app_your_phone
    data:
      title: "🔴 Docker container down"
      message: "{{ trigger.to_state.attributes.friendly_name }} has not been running for 2 minutes."
  • mode: parallel: if Docker itself restarts, several containers stop at once. Each one should get its own message, not just the first.
  • to: "off", not “not on”: a sensor that turns unavailable (Portainer unreachable, container deleted) doesn’t fire. That’s on purpose: it prevents a flood of alerts when Portainer restarts, but it also means a deleted container is silently dropped from monitoring.

Tip: the list of sensors is the weak spot. When you add, rename or remove a container, the list has to change with it, or the new container is never watched and the old one stays as a dead entry. I review it together with the backup job of every new container. A label on the status sensors and a label-based trigger would remove that step, at the cost of setting the label on each new sensor.

5. Running isn’t the same as working: HTTP checks

A container can be “running” while the service inside answers with an error page, or while the reverse proxy in front of it sends traffic nowhere. So every ten minutes Home Assistant also calls the public addresses, the way a visitor would, and checks the HTTP status code. The calls are shell_command entries in configuration.yaml:

shell_command:
  curl_check_nextcloud: "curl -s -o /dev/null -w '%{http_code}' --max-time 10 https://cloud.example.com/status.php"
  curl_check_traefik: "curl -s -o /dev/null -w '%{http_code}' --max-time 10 https://traefik.example.com/dashboard/"

The automation runs each command, reads the status code from the response, and compares it with what that service should answer:

triggers:
  - trigger: time_pattern
    minutes: "/10"
actions:
  - action: shell_command.curl_check_traefik
    response_variable: traefik
  - if:
      - condition: template
        value_template: "{{ traefik.stdout not in ['302', '401'] }}"
    then:
      - action: telegram_bot.send_message
        data:
          title: "🔴 Traefik dashboard check failed"
          message: "Expected a login (302/401), got {{ traefik.stdout }}."
  • Check for the right answer, not just any answer. My Traefik dashboard and wiki sit behind single sign-on, so they must answer with a redirect to the login (302) or 401. A 200 would mean they’re suddenly open to everyone. The check catches a broken login, not just a broken service.
  • Plan for maintenance. My nightly Nextcloud backup puts it into maintenance mode, which answers with 503. Between 02:55 and 04:30 the automation ignores exactly that code for Nextcloud, and only that one.

6. Disk space and the host

A full disk is the classic reason for a whole server to fail at once: databases stop writing, containers crash, logs can’t be written to explain why. The SSH add-on’s disk sensors make this a two-line trigger:

triggers:
  - trigger: numeric_state
    entity_id:
      - sensor.ssh_host_myserver_host_disk_use
      - sensor.ssh_host_myserver_host_disk_use_data
    above: 85
actions:
  - action: telegram_bot.send_message
    data:
      message: "⚠️ {{ trigger.to_state.attributes.friendly_name }} is at {{ trigger.to_state.state }} % used."

85 % gives me days, not minutes, to react. The same sensors go on the dashboard as gauges, next to CPU and memory.

7. Backups and updates

  • Backups: a script on the host writes the state of every backup job into a JSON file every five minutes. The SSH add-on reads that file and creates a sensor per job plus one “backup problem” binary sensor. If that is on for ten minutes (a job failed, a snapshot is older than 26 hours, the backup disk isn’t mounted), I get a message with the reason. The details are in my backup article.
  • Updates: an automation runs my Docker update script over SSH three times a week at 04:00 (docker compose pull and up -d per stack). The container alert above is the safety net: if an update breaks a container, I know before breakfast. For the reverse proxy, a pinned major version keeps that risk small (see my Traefik article).
  • Version changes: for Zigbee2MQTT I also store the last known version in an input_text helper and send a message when it changes, with a reminder to read the release notes. I added it after an update changed how some devices behaved and I only noticed days later.

8. The dashboard

A “Server” view on my main dashboard shows the host gauges (CPU, RAM, both disks), the backup status, and one tile per container. Each tile shows whether the container runs, and tapping its icon restarts it:

type: tile
entity: switch.ssh_host_myserver_docker_vaultwarden
name: Vaultwarden
icon: mdi:restart
icon_tap_action:
  action: perform-action
  perform_action: button.press
  target:
    entity_id: button.ssh_host_myserver_docker_vaultwarden_restart

It’s not meant for daily use. It’s the page I open when a message arrives, to see at a glance whether one container is down or the whole host has a problem, and to restart the obvious candidate from my phone.

What I learned

  • One place for alerts. I read Telegram anyway. A separate monitoring system with its own app would be one more thing I ignore.
  • Check from the outside, too. “Container running” and “service works for a visitor” are two different questions. A container can run perfectly while its login is broken or the proxy points elsewhere.
  • Check the expected answer. A login page that suddenly answers 200 is a security problem, not a success.
  • Give every alert a short delay. Two minutes for containers, ten for backups: real problems stay, restarts don’t wake you up.
  • Clean up after removing a container. Delete its device, take it out of the alert list and the dashboard. Dead entities make a dashboard you stop trusting.

Everything here worked in my setup at the time of writing. Software changes, and your setup is different: back up first, test carefully, and use the steps at your own risk. Details in the legal notice.