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.
101 lines
4.0 KiB
Markdown
101 lines
4.0 KiB
Markdown
# 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:
|
|
|
|
```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=<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>/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`.
|