# 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./data` — the admin edits them | | Theme colours | `config/.site.json` in the repo, baked into the image | | Admin password hash, session secret | `/var/lib/menu-app./site.env` (untracked, mode 600) | | Which image tag runs | `-e tag=` | | Which Site is being deployed | the inventory entry in `hosts` | ## Deploying From the repo root: ```sh ./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=`, 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: ```sh 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: ```sh 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): ```sh 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: ```sh 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.env` is missing, and the playbook says so. Create it once per Site: ```sh 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 ```sh ssh monoco docker start nginx-menu.hopsngrains.com ``` then point Caddy back at it — the cutover flags above with `retire_nginx_snippet=false`.