ha: replace the HAOS VM with a native Podman Quadlet container

The fixed 2 GiB ha_van allocation starved the 4 GB Pi. HA Container now
runs on the host network (http://10.42.0.1:8123): ha/homeassistant.container
installs to /etc/containers/systemd/, config in /srv/homeassistant, host
D-Bus mounted for onboard Bluetooth (needs apparmor=unconfined — Ubuntu's
dbus-daemon mediates per AppArmor label and denies AddMatch to BlueZ —
plus NET_ADMIN/NET_RAW for habluetooth adapter recovery).

Drop the VM-era plumbing: the 10.42.0.50 dhcp-host pin becomes a
host-record for 10.42.0.1, and the legacy-URL DNAT + hairpin masquerade
go away entirely. ha_van.xml retired to git history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Andreas Wrede
2026-07-07 15:46:30 -04:00
co-authored by Claude Fable 5
parent 8ab5a4cbd8
commit 3add42a0cb
6 changed files with 71 additions and 95 deletions
+24 -19
View File
@@ -61,7 +61,7 @@ This directory is the source of truth. The live system files live under `/etc`,
- Cockpit: `https://192.168.10.251:9090` (or `.27`, or ZeroTier). Log in with a Unix account; enable *Administrative access* for action buttons.
- Regulatory domain **CA** (unlocks 5GHz ch149161 @30dBm, no DFS).
- ZeroTier network `d3ecf5726d041b2a`, pushed DNS domain `wrede.pvt` via `192.168.196.115` + `192.168.10.5`.
- Home Assistant (HAOS in libvirt VM `ha_van`) is bridged into `br0` at **`10.42.0.50:8123`** (pinned DHCP lease, name `homeassistant`); legacy URL `http://10.42.0.1:8123` still works via DNAT.
- Home Assistant runs **natively on this host** (Podman Quadlet, host network) at **`http://10.42.0.1:8123`** (name `homeassistant`). The old HAOS VM `ha_van` (`10.42.0.50`) was retired 2026-07-07.
---
@@ -139,13 +139,13 @@ This directory is the source of truth. The live system files live under `/etc`,
The `hbc` binary itself (venv at `~/venvs/hbd`, symlink `~/bin/hbc`) is installed once via the
heartbeat project's own installer — see §4. `deploy.sh` only starts the service once it exists.
### `ha/` — Home Assistant VM
### `ha/` — Home Assistant (native container)
| file | → installs to | purpose |
|---|---|---|
| `ha_van.xml` | *(reference only — `virsh define ha/ha_van.xml` to restore)* | libvirt domain: HAOS VM, virtio NIC bridged onto `br0` (MAC `52:54:00:ad:0a:01`). Disk image lives outside the repo. |
| `homeassistant.container` | `/etc/containers/systemd/homeassistant.container` | Podman Quadlet: HA Container on the host network (`:8123`), config in `/srv/homeassistant`, host D-Bus mounted for onboard Bluetooth. |
Not touched by `deploy.sh` — the VM's LAN address/name come from `ap/van-ap-dnsmasq.conf`
(`dhcp-host``10.42.0.50`, `homeassistant`) and the legacy-URL DNAT from `ap/nftables.conf`.
The `homeassistant` LAN name comes from `ap/van-ap-dnsmasq.conf` (`host-record`
`10.42.0.1`). The retired HAOS VM's domain XML (`ha_van.xml`) lives in git history.
---
@@ -206,20 +206,25 @@ Not touched by `deploy.sh` — the VM's LAN address/name come from `ap/van-ap-dn
- The Access Points card shows both bands (5GHz `hostapd`, 2.4GHz `hostapd-2g`) with per-band client lists and **Restart** buttons; the radio list lives in `vanrouter.js` (`const APS`).
- **Prefer** sets the manual WAN preference (see *Manual preference* above); **Up/Down** connect/disconnect the NM device.
### Home Assistant VM (`ha_van`)
- HAOS runs as a libvirt KVM VM whose NIC is **bridged into `br0`** — it is a first-class LAN
device, not NAT'd behind libvirt's `virbr0`. VanLink + wired clients reach it directly at
`http://10.42.0.50:8123` (or `http://homeassistant:8123` / `homeassistant.local` via mDNS);
ZT clients route in via the ZT-managed `10.42.0.0/24` route. mDNS/SSDP device discovery works
because the VM shares the clients' L2 segment.
- The old NAT-era URL `http://10.42.0.1:8123` keeps working: nftables DNATs it to `10.42.0.50`,
with a hairpin masquerade for same-subnet clients (see comments in `ap/nftables.conf`).
- History: the VM used to sit on libvirt's `default` NAT net (`192.168.122.50`) with a
`/etc/libvirt/hooks/network` hook inserting FORWARD accepts above libvirt's REJECT. That broke
whenever libvirtd re-inserted its chains on restart (hook doesn't fire then) — bridging removed
the whole failure mode. The hook and the libvirt DHCP reservation are gone.
- Operate: `virsh {start,shutdown,domstate} ha_van`; autostart is per libvirt config. Verify:
`curl -s -o /dev/null -w '%{http_code}' http://10.42.0.50:8123/``200`.
### Home Assistant (native Podman container)
- HA Container runs on the host via a **Podman Quadlet** (`ha/homeassistant.container`
`/etc/containers/systemd/`; systemd generates `homeassistant.service`). **Host network**:
HA binds `:8123` directly, so clients use `http://10.42.0.1:8123` (or
`http://homeassistant:8123`); mDNS/SSDP discovery sees the LAN because there's no bridge
or NAT in the way. Config lives in `/srv/homeassistant`.
- **Bluetooth**: host BlueZ (`bluetooth.service`) serves the Pi's onboard `hci0` to HA over
the mounted `/run/dbus` socket — add the Bluetooth integration in HA and it appears.
- No add-on store (that was HAOS's Supervisor): Mosquitto/Zigbee2MQTT-style add-ons become
their own containers/services if ever needed.
- Operate: `systemctl {status,restart} homeassistant`. Update: bump/pull the image
(`podman pull ghcr.io/home-assistant/home-assistant:stable`) and restart. After editing
the `.container` file: `./deploy.sh` (or install + `systemctl daemon-reload`), then
`systemctl restart homeassistant`. Verify:
`curl -s -o /dev/null -w '%{http_code}' http://10.42.0.1:8123/``200`.
- History: HA started as a HAOS VM on wayback (libvirt NAT, then bridged onto `br0` at
`10.42.0.50` with a legacy-URL DNAT), moved to this Pi as an aarch64 VM 2026-07-06, and
went native 2026-07-07 — the fixed 2 GiB VM allocation starved the 4 GB Pi. The domain
XML (`ha/ha_van.xml`) and the DNAT/hairpin nftables rules are in git history.
### Never sleep (lid-closed operation)
- wayback lives lid-closed in the van and must stay up. Stock logind `HandleLidSwitch=suspend` would sleep it on lid close (even on AC). The `power/10-vanlink-nolid.conf` drop-in sets all three lid actions to `ignore`; `deploy.sh` also masks every sleep target so idle / GUI / a stray `systemctl suspend` can't suspend it.
-12
View File
@@ -4,20 +4,8 @@
table ip van_router_nat
delete table ip van_router_nat
table ip van_router_nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
# Home Assistant VM (ha_van) is bridged onto br0 at 10.42.0.50 — clients reach
# it directly. Keep the legacy http://10.42.0.1:8123 URL working for anything
# that bookmarked it (phones, ZT clients).
ip daddr 10.42.0.1 tcp dport 8123 dnat to 10.42.0.50:8123
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.42.0.0/24 oifname != "br0" masquerade
# Hairpin for the legacy 10.42.0.1:8123 DNAT when the client sits on the same
# subnet as the HA VM: without masquerade the VM would reply directly on br0
# from 10.42.0.50 and the client (expecting 10.42.0.1) would drop it. ZT-sourced
# traffic doesn't match and doesn't need it — VM replies route back through us.
ip saddr 10.42.0.0/24 ip daddr 10.42.0.50 tcp dport 8123 oifname "br0" masquerade
}
}
+3 -3
View File
@@ -5,8 +5,8 @@ domain-needed
bogus-priv
dhcp-authoritative
dhcp-range=10.42.0.10,10.42.0.254,255.255.255.0,12h
# Home Assistant VM (libvirt ha_van, bridged onto br0) — pinned address, resolves
# as "homeassistant" via this dnsmasq; HAOS also announces homeassistant.local (mDNS).
dhcp-host=52:54:00:ad:0a:01,10.42.0.50,homeassistant
# Home Assistant runs natively on this host (Podman, host network, :8123) —
# "homeassistant" is just another name for the router address.
host-record=homeassistant,10.42.0.1
dhcp-option=option:router,10.42.0.1
dhcp-option=option:dns-server,10.42.0.1
+10
View File
@@ -54,6 +54,13 @@ if [ ! -f /etc/van-battery/pushover.json ]; then
echo " -> seeded /etc/van-battery/pushover.json (EDIT IT: add Pushover token + user)"
fi
echo "== home assistant =="
# Native HA (Podman Quadlet, replaced the ha_van VM). daemon-reload below
# regenerates homeassistant.service; started (not restarted) at the end so a
# deploy never bounces HA — after editing the .container, restart it manually.
install -D -m0644 ha/homeassistant.container /etc/containers/systemd/homeassistant.container
install -d -m0755 /srv/homeassistant
echo "== hardware watchdog =="
install -D -m0644 power/10-vanlink-watchdog.conf /etc/systemd/system.conf.d/10-vanlink-watchdog.conf
@@ -68,6 +75,9 @@ systemctl daemon-reexec
systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true
systemctl unmask hostapd >/dev/null 2>&1 || true
systemctl enable regdomain.service hostapd hostapd-2g van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal van-ap-watchdog van-ap-watchdog-2g >/dev/null 2>&1 || true
# bluetooth: host BlueZ serves the onboard hci0 to the HA container over D-Bus
systemctl enable --now bluetooth >/dev/null 2>&1 || true
systemctl start homeassistant || echo " -> homeassistant failed to start (podman/quadlet — check journalctl -u homeassistant)"
systemctl restart van-thermal
# Pick up unmanaged-devices changes so NM releases/keeps the right interfaces.
nmcli general reload 2>/dev/null || systemctl reload NetworkManager 2>/dev/null || true
-61
View File
@@ -1,61 +0,0 @@
<!-- Home Assistant OS VM — aarch64 port for the Pi 4 "wan" host.
Same identity as wayback's x86 ha_van: MAC 52:54:00:ad:0a:01 is pinned to
10.42.0.50/"homeassistant" by van-ap-dnsmasq. 2 GiB RAM (Pi has 3.7 total),
machine=virt + AAVMF EFI, virtio disk/net. Fresh 18.1 install, configured
from scratch (a backup-restore from the x86 instance left Core stopped and
Supervisor idle; the old config was minimal, so it was abandoned).
VNC graphics + virtio-gpu: the HAOS CLI lives on the graphical console
(its serial console runs no getty), so this is the only way in without
SSH add-ons — view it via Cockpit's Virtual Machines page. -->
<domain type='kvm'>
<name>ha_van</name>
<uuid>af014c94-de20-4f52-8d6b-438f16cd82e6</uuid>
<description>Home Assistant OS (aarch64)</description>
<memory unit='KiB'>2097152</memory>
<currentMemory unit='KiB'>2097152</currentMemory>
<vcpu placement='static'>2</vcpu>
<os firmware='efi'>
<type arch='aarch64' machine='virt'>hvm</type>
<firmware>
<feature enabled='no' name='enrolled-keys'/>
<feature enabled='no' name='secure-boot'/>
</firmware>
<boot dev='hd'/>
</os>
<features>
<gic version='2'/>
</features>
<cpu mode='host-passthrough' check='none'/>
<clock offset='utc'/>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>destroy</on_crash>
<devices>
<emulator>/usr/bin/qemu-system-aarch64</emulator>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' discard='unmap'/>
<source file='/var/lib/libvirt/images/haos_generic-aarch64-18.1.qcow2'/>
<target dev='sda' bus='scsi'/>
</disk>
<controller type='scsi' index='0' model='virtio-scsi'/>
<interface type='bridge'>
<mac address='52:54:00:ad:0a:01'/>
<source bridge='br0'/>
<model type='virtio'/>
</interface>
<serial type='pty'>
<target type='system-serial' port='0'/>
</serial>
<console type='pty'>
<target type='serial' port='0'/>
</console>
<graphics type='vnc' port='-1' listen='127.0.0.1'/>
<video>
<model type='virtio'/>
</video>
<rng model='virtio'>
<backend model='random'>/dev/urandom</backend>
</rng>
<memballoon model='virtio'/>
</devices>
</domain>
+34
View File
@@ -0,0 +1,34 @@
# Home Assistant Container as a Podman Quadlet (replaced the ha_van HAOS VM
# 2026-07-07 — the fixed 2 GiB VM allocation starved the 4 GB Pi; VM domain XML
# is in git history under ha/ha_van.xml). Installed by deploy.sh to
# /etc/containers/systemd/; systemd generates homeassistant.service from it.
#
# Host networking: HA binds :8123 on the host directly, so the LAN URL is
# http://10.42.0.1:8123 (name: homeassistant) — no bridge port, no DNAT.
# /run/dbus gives HA the host BlueZ stack = the Pi's onboard Bluetooth (hci0).
[Unit]
Description=Home Assistant (Podman container)
Wants=network-online.target bluetooth.service
After=network-online.target bluetooth.service
[Container]
Image=ghcr.io/home-assistant/home-assistant:stable
ContainerName=homeassistant
Network=host
Volume=/srv/homeassistant:/config
Volume=/run/dbus:/run/dbus:ro
Environment=TZ=America/Toronto
# Bluetooth needs two escapes: Ubuntu's dbus-daemon mediates D-Bus per
# AppArmor label and the default containers profile can't send to BlueZ
# (AddMatch denied), and habluetooth wants NET_ADMIN+NET_RAW for adapter
# recovery. HA is rootful + host-net anyway.
PodmanArgs=--security-opt apparmor=unconfined
AddCapability=NET_ADMIN NET_RAW
[Service]
Restart=always
# First start pulls the image (~600 MB) — allow for a slow uplink.
TimeoutStartSec=900
[Install]
WantedBy=multi-user.target