Files
menu/docs/adr/0009-rootless-podman-quadlets.md
T
tanshu 537a4f1e46 Match the deploy system to hops and greatbear
menu-admin was the odd one out: it built from the working directory with a date
tag and a multi-arch manifest, deployed with `-e tag=`, and kept the Docker-era
container name. The other two apps build from `git archive HEAD` and tag with the
commit, so what runs on the host is exactly what git knows about; build only for
the architecture emily actually runs; and use one slug for the container, its
network and its data directory.

The build is now their three lines: `podman build --platform linux/arm64` from
`git archive HEAD`, tagged `:<commit>` and `:latest`, pushed with `podman push`
rather than a manifest. `deploy.sh` pushes the commit first, then refuses a dirty
tracked tree or a HEAD that is not origin/main — which is what makes the commit
tag trustworthy — takes the same `--force` and `--no-push`, and ends in the same
`-u tanshu ... -e image_tag=<commit>`. Its own lint/typecheck/test gate stays,
but behind `--checks`, so a plain deploy behaves as theirs does.

The registry credential moves into vars/default.yml and the role logs in, pulls
and logs out around it, as their roles do. The image is only pulled at deploy
time, so the unit keeps podman's default `Pull=missing` and a reboot never needs
the registry. This is the same plaintext credential the other two repos carry.

One real bug goes with it: the healthcheck gate ran before the handler that
restarts a changed unit, so a run that only changed the unit checked the stale
container. `meta: flush_handlers` before the gate is the fix the sibling repos
already made.

The container, network and data directory are all menu-<site> now. The dotted
Docker-era name and the NetworkAlias it needed are gone, so `podman logs
menu-<site>` and `systemctl --user status menu-<site>` are the same two commands
across all three apps. The Dockerfile keeps its name for the same reason: podman
reads it as readily as a Containerfile, and the siblings use that name too.
2026-10-16 11:10:59 +05:30

2.3 KiB

Sites run as rootless Podman Quadlets behind a shared Caddy

Each Site is deployed as a rootless Podman container managed by a systemd user unit (Quadlet) on emily, reached through the one shared Caddy unit rather than a long-lived rootful Docker container. The image is built by Podman too, from the committed tree, the same way the sibling hops and greatbear apps are built and deployed. This matches how every other service on that host is deployed, and it takes the deployment off Docker entirely.

The app owns its whole serving path, so it needed the smallest deployment of the fleet: one .container file, one .network file, and a block appended to the Caddyfile. Two details carry the design. UserNS=keep-id:uid=1000,gid=1000 maps the image's own node account onto the user that owns the units, which is what keeps data/ and site.env owned by a real host user — plain tar backups, no subuids — and scripts/provision-site-env.sh uses the same mapping so both sides agree on who owns the file. And the Site registers its own network with Caddy by writing a drop-in beside caddy.container, so Caddy never lists its upstreams and never has to be edited when a Site is added or removed.

Considered Options: staying on rootful Docker on monoco, rejected because the fleet is moving and leaving one app behind means two runtimes and two Caddy configs to reason about; running rootful Podman, rejected because it keeps the same root-owned data and loses the plain-backup property; and keeping the Docker-era container name menu-app.<domain>, rejected because a name with dots in it is a liability in DNS and it would have meant a second name (a network alias) for Caddy to proxy to. The inventory name is now the single slug for everything — container, network and data directory are all menu-<site>, so the two commands an operator needs are the same across all three menu apps.

Consequences: the app is not published on a host port at all — only Caddy, on the shared Podman network, can reach it — and deployment, logs and restarts move from docker to systemctl --user / podman. The data root moves to ~/podman-data/menu-<site>, so each Site's content has to be copied across from /var/lib/menu.<domain> once, before DNS points at it.