feat(esphome): pin 2026.8.2, relocate config into backup coverage, rotate creds

ha-dev requested all three on esh-docker-vm (operator-authorized); the stack
had no canonical copy, so it is added to stacks/ rather than edited in place.

Pinned ghcr.io/esphome/esphome:2026.8.2 — it was bare, which is exactly how it
sat on 2025.8.2 for a year: docker pulled latest once at container creation
(2026-04-20, from a layer cached 2025-08-29) and never re-pulled. Every current
Everything Presence sensor failed config validation on that build. Verified
after: esphome version reports 2026.8.2 and the vendor's own Pro package now
validates clean (exit 0, 'Configuration is valid!'), which is the item that
unblocks the six waiting sensors.

Relocated /path/to/esphome/config (the upstream template placeholder, taken
literally by docker) to /opt/docker/conf/esphome, matching the mosquitto
pattern. Copied and checksum-verified all 5763 files before removing the
original, with a tarball kept at /root/pre-change-archive/.

Credentials moved off test/ChangeMe to the vaulted 32-char secret
(esh-docker-vm/esphome-dashboard), passed via a host-only .env so nothing
plaintext enters git.

Three things the job surfaced that were not in the request:

  The directory is 538 MB, not the 3 KB reported — .esphome/platformio is
  508 MB of PlatformIO toolchain and .esphome/build another 31 MB, both
  regenerable. Relocating as-asked would have inflated restic's /opt/docker
  source ~45x against its own ~12 MB budget, so both subtrees are excluded in
  /etc/restic/profiles.yaml. The 3 KB of actual config is now covered, which
  was the point.

  2026.8.2 logs a DEPRECATION for the bare USERNAME/PASSWORD env names and says
  they will stop working in a future release — a silent auth loss on some later
  bump, on a privileged host-network container that can flash any ESP device on
  the LAN. Switched to ESPHOME_USERNAME/ESPHOME_PASSWORD; the warning is gone.

  Device Builder 1.0.0 opens a NEW listener on 0.0.0.0:6055 (remote-build
  peer-link) that 2025.8.2 did not have.

Also fixes deploy-stack.sh: plain 'rsync -a' makes rsync chgrp the destination
as the deploy identity, which since the 2026-09-14 root:docker normalisation is
not root. It failed with 'Operation not permitted' and exit 23 AFTER
transferring content — a loud error on a deploy that had succeeded. --no-o
--no-g lets the setgid bit assign the group instead.
This commit is contained in:
2026-09-14 18:47:54 -07:00
parent 68fa80f44d
commit d1769ed114
4 changed files with 94 additions and 2 deletions
+8 -2
View File
@@ -163,7 +163,13 @@ for entry in "${PAIRS[@]}"; do
tmp_out=$(mktemp) tmp_err=$(mktemp)
rc=0
rsync -az --delete --dry-run \
# --no-o --no-g: the deploy tree is root:docker 2775 (setgid) since the
# 2026-09-14 fleet normalisation, and plain -a makes rsync try to chgrp the
# destination as the deploy identity, which is not root. It fails with
# "chgrp ... Operation not permitted" and exits 23 AFTER transferring the
# content — a loud error on a deploy that actually succeeded. The setgid bit
# already assigns the right group, so rsync should not be fighting it.
rsync -az --no-o --no-g --delete --dry-run \
"${RSYNC_REMOTE[@]}" \
--out-format='%i %n' \
"${EXCLUDES[@]}" "${extra[@]}" \
@@ -285,7 +291,7 @@ for entry in "${PAIRS[@]}"; do
extra=()
[ "$kind" = compose ] && extra+=(--exclude='conf/')
printf 'pushing %s → %s\n' "$src" "$dest"
rsync -az --delete \
rsync -az --no-o --no-g --delete \
"${RSYNC_REMOTE[@]}" \
"${EXCLUDES[@]}" "${extra[@]}" \
"$src" "$dest"
+5
View File
@@ -0,0 +1,5 @@
# Real values live on the host at /opt/docker/compose/esphome/.env (root:docker
# 0640) and in Vaultwarden as `esh-docker-vm/esphome-dashboard`.
# Retrieve with: secret get esh-docker-vm/esphome-dashboard
ESPHOME_USERNAME=admin
ESPHOME_PASSWORD=change-me
+34
View File
@@ -0,0 +1,34 @@
# esphome — ESPHome Device Builder
**Host:** esh-docker-vm (10.0.50.45) · **UI:** http://10.0.50.45:6052 ·
**Config:** `/opt/docker/conf/esphome` · **Credentials:** Vaultwarden
`esh-docker-vm/esphome-dashboard`
Firmware build + OTA dashboard for the ESH ESP32/ESP8266 fleet. As of
ESPHome 2026.6.0 the old dashboard is replaced by **ESPHome Device Builder
1.0.0**, a ground-up rewrite — same function, different UI. Not a fault.
## Deploy
```bash
scripts/deploy-stack.sh esh-docker-vm esphome
ssh infra-ops@esh-docker-vm 'cd /opt/docker/compose/esphome && sudo -n docker compose up -d'
```
The `.env` is never synced in either direction (see the root `CLAUDE.md`
exclusion list). It is created on the host once, from the vault.
## Three things not to undo
- **`privileged: true` and `network_mode: host` are required.** USB flashing
needs the first, mDNS discovery the second. Removing either breaks device
adoption and OTA.
- **Keep the image tag pinned.** A bare `ghcr.io/esphome/esphome` is how this
container sat on 2025.8.2 for a year — docker pulled `latest` once at
creation and never again. Every current Everything Presence sensor (Pro, One,
Lite) failed `esphome config` on that build.
- **`.esphome/platformio` and `.esphome/build` are excluded from restic**
(`/etc/restic/profiles.yaml`). They are 539 MB of regenerable PlatformIO
toolchain and build artifacts against 3 KB of actual config; without the
exclude, relocating this dir inflates the `/opt/docker` backup source ~45×.
PlatformIO re-downloads them on demand.
+47
View File
@@ -0,0 +1,47 @@
# ESPHome Device Builder — IoT firmware dashboard for the ESH ESP32/ESP8266 fleet.
# Host: esh-docker-vm (10.0.50.45). UI: http://10.0.50.45:6052
#
# ⚠ privileged + network_mode: host are REQUIRED and must not be "hardened" away:
# USB flashing needs the former, mDNS device discovery the latter. Removing
# either breaks adoption and OTA.
#
# ⚠ The image tag is pinned deliberately. A bare `ghcr.io/esphome/esphome`
# drifted this container a full year: docker pulled `latest` once when the
# container was created (2026-04-20, from a layer already cached 2025-08-29) and
# never re-pulled, leaving it on 2025.8.2 — twelve releases behind — which
# silently failed config validation for every current Everything Presence
# sensor. Bump this line on purpose; do not un-pin it.
services:
esphome:
container_name: esphome
image: ghcr.io/esphome/esphome:2026.8.2
volumes:
# Was /path/to/esphome/config — the upstream template placeholder, taken
# literally by docker. Nothing was broken (compose and the container
# agreed) but /path/to is in none of restic's sources, so the configs and
# secrets.yaml were absent from file-level backup. Moved 2026-09-14 to
# match the sibling mosquitto pattern.
- /opt/docker/conf/esphome:/config
- /etc/localtime:/etc/localtime:ro
restart: always
privileged: true
network_mode: host
environment:
# Values live in .env beside this file on the host (root:docker 0640) and
# in Vaultwarden at esh-docker-vm/esphome-dashboard. Never inline them
# here — this file is git-tracked.
#
# ⚠ Use the ESPHOME_-prefixed names, not bare USERNAME / PASSWORD. 2026.8.2
# still honours the bare names but logs a DEPRECATION on every start and
# says they "will stop working in a future release" — i.e. a silent auth
# loss on some later bump, on a privileged host-network container that can
# flash firmware to anything on the LAN. Observed in the container log on
# the 2025.8.2 -> 2026.8.2 upgrade, 2026-09-14.
- ESPHOME_USERNAME=${ESPHOME_USERNAME}
- ESPHOME_PASSWORD=${ESPHOME_PASSWORD}
labels:
- homepage.group=Apps
- homepage.name=ESPHome
- homepage.icon=si-esphome
- homepage.description=IoT firmware dashboard (ESP32/ESP8266)
- homepage.href=http://10.0.50.45:6052