# The menu deployment Lives here, beside the app it deploys. One run deploys a Site: it starts the app's container and points the Site's web address at it. There is no older system to keep serving, so there is no staged rollout and nothing to retire — deploying an earlier tag is the rollback. **The app's data root is the directory the old static menus were served from** (`/var/lib/menu.`), on purpose, so the app inherits the location instead of a second one. The app only reads and writes `data/` and `site.env` inside it. If you later want the old menu files gone, delete those files — not the directory, which now holds the live content. ## What lives where | Thing | Where | | -------------------------------------- | ---------------------------------------------------------------------- | | Name, tagline, icon, categories, menus | on the server under `/var/lib/menu./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./site.env` (untracked, mode 600) | | Which image tag runs | `-e tag=` | | Which Site is deployed | the inventory entry in `hosts` | | The web address it serves | `host:` in `vars/.yml` | ## 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 whose fix is `sudo chown -R root:root /etc/ssh/ssh_config.d`, not something this playbook works around.) Adding a Site is one line in `hosts`, a copy of `vars/hopsngrains.yml`, and a `config/.site.json`. The image is shared, so the config has to be in it: a new Site needs a new tag. ## Checking it landed ```sh npm run check-site -- https://menu.hopsngrains.com ``` Read-only, exits non-zero on failure. Then from a phone: the landing page, a Section page, the page counter, tap-to-zoom, and signing into `/admin` to publish something. ## The admin password The app starts with the admin disabled while `/var/lib/menu./site.env` is missing, and the playbook says so rather than failing. Create it once per Site; run it again with `--force` to reset the password (a fresh session secret signs everyone out). There is no minimum password length. ```sh scp scripts/provision-site-env.sh monoco:/tmp/ ssh -t monoco bash /tmp/provision-site-env.sh /var/lib/menu.hopsngrains.com ssh monoco docker restart menu-app.hopsngrains.com ``` The `-t` matters: the script prompts for the password, and without a terminal on the remote side the prompt cannot be read, so it would sit there looking frozen. With no image argument it borrows the one the running container uses. ## Rolling back Deploy the previous tag: ```sh ./deploy.sh ``` `docker logs menu-app.` is where a failed container says why.