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.
This commit is contained in:
tanshu committed 2026-10-16 11:10:59 +05:30
1 parent e68f8ab00d
commit 537a4f1e46
21 files changed
+407 -307

No files matched your search

+12 -9
View File
@@ -2,8 +2,9 @@
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 a `Containerfile`. This matches how every other service on that host is
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
@@ -19,13 +20,15 @@ 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 naming the Quadlet
unit after the container (`menu-app.<domain>`), rejected because the dots make
the unit name awkward and the name fragile in DNS — the unit is `menu-<site>`
while the container keeps its old name for the scripts that look it up.
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
**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.
+8 -8
View File
@@ -12,7 +12,7 @@ this path.
| Thing | Where |
| --------------- | ----------------------------------------------------------------------------------------- |
| The app | user unit `menu-<site>.service` on `emily`, container `menu-app.<site>`, image `registry.tanshu.com/menu-admin:<tag>` |
| The app | user unit `menu-<site>.service` on `emily`, container `menu-<site>`, image `registry.tanshu.com/menu-admin:<commit>` |
| Menu content | `~/podman-data/menu-<site>/data` — `originals/`, `drafts/`, `published/` |
| Admin | `https://<vhost>/admin` |
| The password | `~/podman-data/menu-<site>/site.env` (mode 600, never in git) |
@@ -52,7 +52,7 @@ HASH=$(printf '%s' "$PW" | node -e "let s='';process.stdin.setEncoding('utf8');p
SECRET=$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))")
printf 'ADMIN_PASSWORD_HASH=%s
SESSION_SECRET=%s
' "$HASH" "$SECRET" | ssh emily "podman run --rm -i --userns=keep-id:uid=1000,gid=1000 -v ~/podman-data/menu-hopsngrains:/data --entrypoint sh registry.tanshu.com/menu-admin:2026-09-22 -c 'umask 077 && cat > /data/site.env'"
' "$HASH" "$SECRET" | ssh emily "podman run --rm -i --userns=keep-id:uid=1000,gid=1000 -v ~/podman-data/menu-hopsngrains:/data --entrypoint sh registry.tanshu.com/menu-admin:<commit> -c 'umask 077 && cat > /data/site.env'"
ssh emily systemctl --user restart menu-hopsngrains
```
@@ -70,7 +70,7 @@ the icon (under 256 KB; the upload replaces the bundled mark and "Remove the
uploaded icon" brings it back). None of this changes your web addresses.
**Change the colours.** Those are still `config/<site>.site.json`, so they need a
rebuild and push (`./deploy.sh <tag>`) and the playbook with the new tag.
new build and push (`./deploy.sh`).
**Fix a missing image.** If a Page's thumbnail or viewing image is lost while its
Original survives, the app can rebuild them:
@@ -79,7 +79,7 @@ Original survives, the app can rebuild them:
podman run --rm --userns=keep-id:uid=1000,gid=1000 -e MENU_ROOT=/data \
-e MENU_SITE_CONFIG=/app/config/hopsngrains.site.json \
-v ~/podman-data/menu-hopsngrains:/data \
--entrypoint node registry.tanshu.com/menu-admin:<tag> src/cli/repair.ts
--entrypoint node registry.tanshu.com/menu-admin:<commit> src/cli/repair.ts
```
`src/cli/verify.ts` does the same inventory without changing anything, and is
@@ -92,15 +92,15 @@ they would see the files as a different user and fail to write.
| --- | --- |
| Landing page says the menus are being updated | no Section has a Current Menu Set — publish one in the admin |
| A Section 404s | that Section has no Current set, or the URL is misspelled |
| Sign-in rejects the right password | `podman logs menu-app.<site>`, then check `site.env` exists and is owned by `tanshu` |
| Sign-in rejects the right password | `podman logs menu-<site>`, then check `site.env` exists and is owned by `tanshu` |
| Admin is missing entirely | `site.env` was unreadable at boot, so the admin is disabled: run the provision script and `systemctl --user restart menu-<site>` |
| A photo uploads but looks wrong | the browser shrinks to 3000px before upload; that is expected, and the Original is kept |
| The container will not start | `systemctl --user status menu-<site>` and `podman logs menu-app.<site>` — a fatal integrity problem makes it refuse to serve a data directory it cannot trust |
| The container will not start | `systemctl --user status menu-<site>` and `podman logs menu-<site>` — a fatal integrity problem makes it refuse to serve a data directory it cannot trust |
| The unit is "not found" | its `.network` file is missing, so Quadlet refused to generate it: check `~/.config/containers/systemd/` |
| Caddy reload failed during a deploy | nothing is down: Caddy validates before applying. Read the error in the playbook output |
| Everything is slow | check the tag is current; a Viewing image is ~1600px, so a Section page should not be megabytes |
**Logs.** `podman logs -f menu-app.<site>`, or `journalctl --user -u menu-<site>
**Logs.** `podman logs -f menu-<site>`, or `journalctl --user -u menu-<site>
-f` for the unit. The app logs JSON lines, one per event, including every upload,
publish, restore and failed sign-in.
@@ -129,7 +129,7 @@ to trip over and no `podman unshare` needed.
## Rolling back
Deploy the previous tag — `./deploy.sh <previous-tag>` — which rebuilds the Site
at that version. `podman logs menu-app.<site>` is where a container that will not
at that version. `podman logs menu-<site>` is where a container that will not
start says why.
The Docker-era container was left on `monoco`, stopped and not removed, so the