A lightweight Docker-based exporter that polls the Headscale API and publishes node status to Home Assistant via MQTT Discovery.
- π’ Real-time online/offline status for all Headscale nodes
- π‘ Auto-discovery in Home Assistant β no manual entity configuration
- π Human-readable "last seen" timestamps (e.g.
5m ago,2d 3h ago) - π€ User, Tailnet IP, and approved routes per node
- π₯ Group mapping β assign nodes to groups for filtered dashboard cards
- π Configurable poll interval
- π Connects to Headscale internally via Docker network
- Headscale running in Docker
- Home Assistant with Mosquitto MQTT broker add-on
- MQTT reachable from the Headscale VPS (via Tailnet recommended)
Headscale API β exporter.py β MQTT Broker β Home Assistant
Each node becomes a binary_sensor in Home Assistant with the following attributes:
| Attribute | Description |
|---|---|
online |
true/false |
last_seen |
ISO timestamp |
last_seen_ago |
Human readable e.g. 5m ago |
tailnet_ip |
First IPv4 tailnet address |
ip_addresses |
All IP addresses |
user |
Headscale user display name |
group |
Group assigned via USER_GROUPS mapping |
approved_routes |
Advertised subnet routes |
hostname |
Machine hostname |
git clone https://github.com/HybridRCG/headscale-ha-exporter.git
cd headscale-ha-exportercp .env.example .env
nano .envFill in your values:
| Variable | Description |
|---|---|
HEADSCALE_API_URL |
Internal Headscale URL e.g. http://headscale:8080 |
HEADSCALE_API_KEY |
Headscale API key |
MQTT_BROKER |
MQTT broker IP or hostname |
MQTT_PORT |
MQTT port (default: 1883) |
MQTT_USER |
MQTT username |
MQTT_PASSWORD |
MQTT password |
POLL_INTERVAL |
Poll interval in seconds (default: 30) |
USER_GROUPS |
JSON mapping of user display names to group names |
The USER_GROUPS environment variable maps Headscale user display names to group names. This adds a group attribute to each node entity in Home Assistant, allowing you to create filtered dashboard cards per group.
USER_GROUPS={"Marius Viljoen":"MVSolar","Rika Viljoen":"MVSolar","Riaan Grobler":"Admin"}Any user not in the mapping will be assigned the group Other.
The exporter needs to be on the same Docker network as Headscale. Update docker-compose.yml with your Headscale network name:
networks:
headscale_network:
external: true
name: headscale_headscale_defaultTo find your Headscale network name:
docker network ls | grep headscaledocker compose build
docker compose up -d
docker logs headscale-exporter -fOnce running, entities will auto-discover in Home Assistant under:
Settings β Devices & Services β MQTT β Devices β Headscale Nodes
Each node appears as binary_sensor.headscale_<nodename>.
If your Home Assistant MQTT broker is accessed via the Tailnet (recommended over exposing port 1883 to the internet), you need to allow the Headscale VPS to reach Home Assistant on port 1883.
Add the following rule to your Headscale ACL policy:
{
"action": "accept",
"src": [
"hs-exit"
],
"dst": [
"ha:1883"
]
}Replace hs-exit with your VPS node name and ha with your Home Assistant node name in Headscale.
To find your node names:
headscale nodes listThis is more secure than exposing MQTT port 1883 directly to the internet β all traffic stays encrypted within the Tailnet.
A ready-made Lovelace dashboard card is available in DASHBOARD.md showing:
- Online/Offline/Total summary
- Filter buttons (All / Online / Offline)
- Sortable node table
- Group filtered cards (MVSolar, Dyna, IT & Admin)
MIT
The exporter exposes a health check endpoint on port 8099 (configurable via HEALTH_PORT):
curl http://your-vps-ip:8099/healthReturns:
{
"status": "ok",
"version": "0.2.1-beta",
"uptime_since": "2026-03-06T07:07:59Z",
"last_poll": "2026-03-06T07:08:29Z",
"nodes_total": 17,
"nodes_online": 10,
"poll_count": 2,
"mqtt_connected": true
}Add HEALTH_PORT=8099 to your .env to change the port.
The exporter saves node online timestamps to /app/data/node_state.json so that connection durations survive container restarts.
The data/ directory is mounted as a volume in docker-compose.yml:
cat >> /headscale/headscale-ha-exporter/README.md << 'EOF'
## State Persistence
The exporter saves node online timestamps to `/app/data/node_state.json` so that connection durations survive container restarts.
The `data/` directory is mounted as a volume in `docker-compose.yml`:
```yaml
volumes:
- ./data:/app/dataThis means:
- β
Container restarts preserve
connected_sincetimestamps - β
status_infoattribute shows correctUp Xh Xmafter restart - β State is saved once per poll cycle
The data/ directory is created automatically on first run.
Each binary_sensor.headscale_<nodename> entity exposes the following attributes:
| Attribute | Description | Example |
|---|---|---|
hostname |
Machine hostname | macbookpro |
user |
Headscale user display name | Riaan Grobler |
group |
Group from USER_GROUPS mapping |
Admin |
last_seen |
ISO timestamp of last seen | 2026-03-06T07:00:00Z |
last_seen_ago |
Human readable last seen | 5m ago |
status_info |
Up duration or last seen ago | Up 2h 30m or 1d 2h ago |
tailnet_ip |
First IPv4 tailnet address | 100.64.0.4 |
ip_addresses |
All IP addresses | 100.64.0.4, fd7a::4 |
approved_routes |
Advertised subnet routes | 192.168.1.0/24 |
connected_since |
ISO timestamp of when node came online | 2026-03-06T07:00:00Z |
connected_duration |
How long node has been online | 2h 30m |
The exporter saves node online timestamps to /app/data/node_state.json so that connection durations survive container restarts.
The data/ directory is mounted as a volume in docker-compose.yml:
volumes:
- ./data:/app/dataThis means:
- β
Container restarts preserve
connected_sincetimestamps - β
status_infoattribute shows correctUp Xh Xmafter restart - β State is saved once per poll cycle
The data/ directory is created automatically on first run.
Each binary_sensor.headscale_<nodename> entity exposes the following attributes:
| Attribute | Description | Example |
|---|---|---|
hostname |
Machine hostname | macbookpro |
user |
Headscale user display name | Riaan Grobler |
group |
Group from USER_GROUPS mapping |
Admin |
last_seen |
ISO timestamp of last seen | 2026-03-06T07:00:00Z |
last_seen_ago |
Human readable last seen | 5m ago |
status_info |
Up duration or last seen ago | Up 2h 30m or 1d 2h ago |
tailnet_ip |
First IPv4 tailnet address | 100.64.0.4 |
ip_addresses |
All IP addresses | 100.64.0.4, fd7a::4 |
approved_routes |
Advertised subnet routes | 192.168.1.0/24 |
connected_since |
ISO timestamp of when node came online | 2026-03-06T07:00:00Z |
connected_duration |
How long node has been online | 2h 30m |