feat(podman-host): SmartHome rootless Podman + quadlet host (CT 6004, ADR-0009 Phase 1) - #4
Merged
Merged
Conversation
… ADR-0009 Phase 1) Phase 1 of the Podman migration (Homelab#285), on the platform landed in Homelab#284. Adds the stack's container HOST — deliberately generic, not a Leapmotor box. Leapmotor Mate is simply the first service to live on it, chosen as the pilot because it's the smallest live stack (one container, no NFS, state on local disk) so the fiddly rootless plumbing proves itself where the blast radius is smallest. Future SmartHome services join as extra quadlets rather than as new one-app Docker CTs. - podman-host.lxc.yaml — CT 6004, IN-BLOCK for this stack (6000–6099), unlike the CT 4100 it replaces which was adopted in-place out-of-block. Getting Mate back inside the block is part of the point. - podman-host/quadlets/leapmotor-mate.container — converted from leapmotor-mate/compose.yml. Bind mounts under the rootless user's home rather than named volumes, because the migration seeds 22 MB of EXISTING state and a host path is far easier to seed, inspect and back up. - MATE_AUTH_PASSWORD wired as a podman secret (registered in Bitwarden SM + the superproject's secrets.env.template). MATE_SECRET_KEY deliberately NOT set: Mate auto-generates it into its data dir when unset — which is exactly what CT 4100 does today (its .env has the key blank) — and it must stay paired with the migrated database or the stored credentials become undecryptable. Cutover needs a SHORT downtime, and that's an upstream constraint rather than a Podman one: Leapmotor allows ~one active session per account, so the old and new Mate cannot both run logged-in at once. The host and unit are built side-by-side safely; the handover itself is exclusive. Runbook in podman-host/README.md, with rollback = start CT 4100 again (the copy duplicates state, never moves it). CT 4100 is left STOPPED, not destroyed — deleting it is a deliberate manual call once the new host has proven itself. Refs Homelab#285, Homelab#283, ADR-0009
Both surfaced immediately on the first real deploy; neither is findable by
reading the compose file it was converted from.
1. Podman does NOT create missing bind-mount source directories (Docker does),
so the unit crash-looped: "Error: statfs /home/podman/leapmotor/certs: no
such file or directory", six restarts then "start request repeated too
quickly". Fixed with an ExecStartPre mkdir so the unit is self-sufficient —
a first start creates empty dirs (Mate boots to its setup wizard) and the
migration later seeds them with CT 4100's real state.
2. HealthCmd cannot carry a quoted command, so the healthcheck is REMOVED.
Quadlet takes systemd's word-split value and rejoins it, destroying inner
quoting however it's written. Two attempts, both verified broken live:
- bare inner quotes -> podman received an unterminated string; the check
failed every 30s with `/bin/sh: Syntax error: Unterminated quoted string`
and the container sat permanently "unhealthy" while the app served 200;
- escaped \" quotes -> the backslashes survived literally.
The compose healthcheck needs a quoted python one-liner (the image ships no
curl/wget), so it cannot be expressed as a quadlet. Restart=always already
covers process death; what's lost is hung-but-alive detection, and a
permanently-unhealthy container is worse than none. Restoring one needs a
QUOTE-FREE command (e.g. `HealthCmd=python /opt/health.py`), which requires
the provisioner to render non-quadlet assets — a platform change, not a
quadlet tweak.
Verified on CT 6004 (hpe-01, 10.40.207.170): unit active, container up, podman
secret seeded, app HTTP 200, and boot survival 4s after an LXC restart with
podman-auto-update.timer active.
Refs Homelab#285
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of the Podman migration — Homelab #285, on the platform landed in Homelab #284 / ADR-0009.
What this adds
The stack's container host — deliberately generic, not a Leapmotor box. Leapmotor Mate is simply the first service to live on it; further SmartHome services join as extra quadlets in
podman-host/quadlets/rather than as new one-app Docker CTs. Mate was chosen as the pilot because it's the smallest live stack (one container, no NFS mount, all state on local disk), so the fiddly rootless plumbing proves itself where the blast radius is smallest.podman10000:50000, verified against the LXC's own uid mapSecrets
MATE_AUTH_PASSWORDis wired as a podman secret (registered in Bitwarden SM and the superproject'ssecrets.env.template), not an env file.MATE_SECRET_KEYis deliberately not set. Mate auto-generates it into its data dir when the variable is absent — which is exactly what CT 4100 does today, since its.envhas the key blank — and it must stay paired with the migrated database or the stored account credentials become undecryptable. Setting it here would silently break the migration.The cutover needs a short downtime — an upstream constraint, not a Podman one
Leapmotor allows ~one active session per account, so the old and new Mate cannot both run logged-in simultaneously; they'd keep kicking each other out. The host and unit are built side-by-side safely (image pulled, unit valid), but the handover itself is exclusive.
Second constraint: 22 MB of live state in CT 4100's
/opt/leapmotor/data— SQLite telemetry history, uploaded mTLS certs, and the generated secret key. It moves as one piece. MQTT settings live in that same SQLite DB (configured via Mate's UI), so they come across with the data and neither the broker nor Home Assistant needs re-wiring.Full runbook in
podman-host/README.md. Rollback ispct start 4100+docker start leapmotor-mate— the migration duplicates state rather than moving it, so the old CT keeps its own untouched copy.CT 4100 is left STOPPED, not destroyed. Deleting it is a deliberate manual call once the new host has proven itself.
Note on applying this
The engine has no per-member converge filter, so
converge stacks/SmartHome --applywould also touch two things this change has no business touching: a tag drift-fix on CT 4100 and aSetConfigon the adopted, describe-only HA VM 2000. CT 6004 was therefore applied from an isolated stack dir containing only this member. Worth a follow-up (--only <member>) if this pattern recurs.🤖 Generated with Claude Code