Files
menu/ansible
tanshu 75b6492493 Move the deployment into the app repo, and build it with podman
The playbook, its roles, its vars and a new inventory now live in ansible/,
beside the app they deploy, the way the other apps on this machine are laid out.
The inventory follows the same shape: one entry per Site, all on the same box,
each naming its own vars file through var_file, so adding a Site is one line and
a file rather than a command-line flag. ansible_host is written as the
resolvable name rather than an ssh alias so the inventory works on any machine.

The image is now built with podman as a multi-arch manifest and pushed to
registry.tanshu.com/menu-admin, mirroring the Makefile used elsewhere on this
machine: build-production, plus build-check and build-check-local for a build
that does not push. deploy.sh runs the checks, builds, pushes, then runs the
playbook for the Site named by SITE, with SKIP_DEPLOY for a push without a
deploy.

ESLint comes out again: this project's typescript@7 has no JavaScript compiler
API, so typescript-eslint cannot install, and linting only the JavaScript was
not worth keeping. Prettier stays, and the README records why ESLint is absent
and what would bring it back.
2026-09-22 23:18:18 +05:30
..

The menu deployment

Lives here, beside the app it deploys, the way the other apps on this machine do. It owns the menu-app container and the Site's snippet in the shared Caddyfile.

What lives where

Thing Where
Name, tagline, icon, categories, menus on the server, under /var/lib/menu-app.<site>/data — the admin edits them
Theme colours config/<site>.site.json in the repo, baked into the image
Admin password hash, session secret /var/lib/menu-app.<site>/site.env (untracked, mode 600)
Which image tag runs -e tag=<tag>
Which Site is being deployed the inventory entry in hosts

Deploying

From the repo root:

./deploy.sh 2026-09-22                 # checks, podman build+push, then deploys
SKIP_DEPLOY=1 ./deploy.sh 2026-09-22   # build and push only

deploy.sh runs make build-production TAG=<tag>, which builds a multi-arch podman manifest (linux/amd64,linux/arm64) and pushes latest and the tag to registry.tanshu.com/menu-admin. Then it runs this playbook against the Site named by SITE (default hopsngrains).

By hand:

make build-production TAG=2026-09-22
cd ansible
ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains -e tag=2026-09-22

Every Site runs on the same box, so hosts has one entry per Site pointing at the same ansible_host with its own var_file. ansible_host is written as the resolvable name rather than an ssh alias, so the inventory works whatever a machine's ~/.ssh/config says. (ssh on some machines complains about /etc/ssh/ssh_config.d/20-systemd-ssh-proxy.conf being owned by nobody; that is a local file-permission bug with the fix sudo chown -R root:root /etc/ssh/ssh_config.d, not something this playbook works around.)

The first run, and cutover

The first run targets a temporary vhost, so the live site keeps serving:

ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains \
  -e tag=2026-09-22 -e vhost=menu-new.hopsngrains.com

menu-new.hopsngrains.com needs a DNS A record pointing at the same address as the live site before this runs: Caddy fetches a certificate for the name, which needs it to resolve publicly.

Then, from the outside (read-only, exits non-zero on failure):

npm run check-site -- https://menu-new.hopsngrains.com

And from a phone on restaurant wifi: the landing page, a Section page, the page counter, tap-to-zoom, and signing into /admin to publish something.

Cutover is the same playbook with the real vhost and the two retire flags:

ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains \
  -e tag=2026-09-22 -e vhost=menu.hopsngrains.com \
  -e retire_nginx_snippet=true -e retire_nginx_container=true

That repoints the real vhost at the app, removes the snippet the old nginx-menu playbook wrote for that hostname, and stops the old container. The old container and /var/lib/menu.hopsngrains.com are kept, not deleted: deleting 319 MB of the only copy of the old menus is a human decision, made once the new site has been trusted.

The admin password

The app starts with the admin disabled when /var/lib/menu-app.<site>/site.env is missing, and the playbook says so. Create it once per Site:

scp scripts/provision-site-env.sh monoco:/tmp/
ssh monoco bash /tmp/provision-site-env.sh /var/lib/menu-app.hopsngrains.com
ssh monoco docker restart menu-app.hopsngrains.com

Rolling back

ssh monoco docker start nginx-menu.hopsngrains.com

then point Caddy back at it — the cutover flags above with retire_nginx_snippet=false.