Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TdvJYoY8kc8bmBck89U2i6
healthcheck-container
Small Flask service for remote devices (Raspberry Pi in Docker) that answers
/alive for container monitoring and, on top of that, watches the network the
device sits on: internet reachability, DNS, public IP changes, Wi-Fi link
quality, Wi-Fi networks in range, and host reboots. Metrics go to SQLite and
are shown on a dashboard at /. Optionally the same values are published to an
MQTT broker with Home Assistant discovery, so each device shows up in Home
Assistant as a device with sensors and no manual configuration.
Endpoints
| Path | Purpose |
|---|---|
/alive |
Unchanged health check, returns {"alive": true} |
/ |
Dashboard: status, Wi-Fi, graphs, timeline |
/api/status |
Latest state of every monitor as JSON |
/api/wifi |
Interface list, link details, last scan results |
/api/events?limit=200 |
Timeline events, newest first |
/api/samples?prefix=reach.&range=3600 |
Bucketed samples for graphs |
Running
The compose file pulls the published multi-arch image
hiimmilan/health-check (amd64, arm64, arm/v7):
docker compose pull && docker compose up -d
Then open http://<device>:9999/.
Wi-Fi needs two things from Docker:
network_mode: host, otherwise the container has no wireless interface at all.cap_add: NET_ADMIN, otherwise link and interface info work but scanning fails with a permission error that is shown on the dashboard.
Without WIFI_INTERFACE the dashboard lists the interfaces it can see and
nothing is scanned. Set the variable to one of the listed wireless interfaces
and restart the container.
Storage
Samples and events are stored in SQLite at DB_PATH (default
/data/healthcheck.db). The compose file mounts a named volume at /data, so
the database survives docker compose down, image rebuilds and up -d. Only
docker compose down -v or docker volume rm deletes it. A bind mount works
as well, for example - /opt/healthcheck:/data.
Samples older than SAMPLE_RETENTION_DAYS (default 7) are deleted hourly.
Events are kept. Wi-Fi scan results are held in memory only and never written.
Configuration
All settings are environment variables. Intervals are in seconds.
| Variable | Default | Meaning |
|---|---|---|
PORT |
9999 |
HTTP port |
DB_PATH |
/data/healthcheck.db |
SQLite file |
SAMPLE_RETENTION_DAYS |
7 |
How long graph samples are kept |
DEVICE_NAME |
hostname | Shown on the dashboard and in Home Assistant |
DEVICE_ID |
slug of DEVICE_NAME |
Used in MQTT topics and unique ids |
REACH_TARGETS |
1.1.1.1:443,8.8.8.8:443,9.9.9.9:443 |
TCP connect targets, host:port |
REACH_INTERVAL |
30 |
|
REACH_TIMEOUT |
3 |
Connect timeout per target |
DNS_NAME |
cloudflare.com |
Name resolved through the system resolver |
DNS_INTERVAL |
60 |
|
PUBLIC_IP_URLS |
ipify, ifconfig.me, icanhazip | Tried in order, first answer wins |
PUBLIC_IP_INTERVAL |
300 |
|
PUBLIC_IP_TIMEOUT |
5 |
|
UPTIME_INTERVAL |
60 |
Reboot detection via /proc/uptime |
WIFI_INTERFACE |
empty | Wireless interface to use, empty disables Wi-Fi |
WIFI_LINK_INTERVAL |
30 |
Signal, bitrate, retries of the current link |
WIFI_SCAN_INTERVAL |
60 |
Scan for networks in range |
MQTT_HOST |
empty | Broker host, empty disables MQTT |
MQTT_PORT |
1883 |
|
MQTT_USERNAME |
empty | |
MQTT_PASSWORD |
empty | |
MQTT_DISCOVERY_PREFIX |
homeassistant |
Must match the MQTT integration setting |
LOG_LEVEL |
INFO |
Events on the timeline
- Internet unreachable / reachable again (all targets failed, then any succeeded)
- DNS resolution failed / working again
- Public IP is X, Public IP changed from X to Y
- Wi-Fi connected, disconnected, roamed to another BSSID
- Host rebooted
- Health check service started
Home Assistant
Set MQTT_HOST (and credentials if the broker needs them). On connect the
service publishes retained discovery messages under
homeassistant/<component>/healthcheck_<device_id>/<key>/config, all pointing
to one device, then publishes state to healthcheck/<device_id>/state and
availability to healthcheck/<device_id>/availability. When Home Assistant
restarts it announces itself on homeassistant/status and the service
re-publishes discovery.
Entities per device:
- Internet, DNS, Wi-Fi (binary sensors,
connectivityclass) - Internet latency, DNS latency (ms)
- Public IP
- Host uptime
- Wi-Fi SSID, BSSID, channel, signal (dBm), TX bitrate (Mbit/s), networks in range
Development
python3 -m unittest discover -s tests
DB_PATH=./data/hc.db python3 app.py
Publish a new image for all Raspberry Pi architectures:
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \
-t hiimmilan/health-check:latest -t hiimmilan/health-check:2.0.0 --push .