Deploys the menu app for one Site: pulls the tag, creates the data root, runs the container against a config baked into the image, waits for health, verifies the data directory, and points Caddy at the app with a marker it can update in place. The parallel run uses a temporary hostname; cutover sets the retire flags.
menu-admin playbook
Deploys the menu app for one Site, beside the existing nginx container at first
and in place of it after cutover. Copy this directory to
ansible-playbooks/menu-admin/ and run it from there, so playbook_dir and the
inventory match the other playbooks.
This playbook owns the menu-app container and the Site's snippet in the shared
Caddyfile. It does not touch the app's source, the image, or the other Sites'
containers.
What lives where
| Thing | Where |
|---|---|
| Sections, brand, theme | config/<site>.site.json in the app repo, baked into the image |
| Menu content, Drafts, Originals | /var/lib/menu-app.<site>/data on the host |
| Admin password hash, session secret | /var/lib/menu-app.<site>/site.env (untracked, mode 600) |
| Which image tag runs | -e tag=<tag> |
Sections are deliberately not editable on the server: changing them is a commit to the app repo, a new image, and a redeploy (ADR 0005).
Deploying
# 1. Build and push from the app repo
./deploy.sh 2026-09-22 # -> registry.tanshu.com/menu-admin:2026-09-22
# 2. First run: a temporary hostname, so the live site keeps serving
ansible-playbook -l monoco -e site=hopsngrains -e tag=2026-09-22 playbook.yml
# 3. The admin needs a password before it can be used
ssh monoco scripts/provision-site-env.sh /var/lib/menu-app.hopsngrains.com
The first run creates /var/lib/menu-app.hopsngrains.com, starts the container
on the menu-app.hopsngrains.com_net network, adds a snippet for
menu-new.hopsngrains.com to the shared Caddyfile, and reloads Caddy. The
existing menu.hopsngrains.com vhost still points at the old nginx container.
site.env is deliberately not created by Ansible: the password is typed by a
human on the server. Until it exists the admin is disabled and the playbook says
so — the public menu still serves.
Checking the parallel run
From a phone on restaurant wifi, not from the server:
https://menu-new.hopsngrains.com/lists the Sections that have a current menu.- A Section page scrolls, the pour line tracks, tapping a page opens it full width.
- The admin signs in, uploads a real photo, reorders, previews, publishes.
- After publishing, the phone shows the new menu and the old set appears under "Older menus".
Cutover
ansible-playbook -l monoco -e site=hopsngrains -e tag=2026-09-22 \
-e vhost=menu.hopsngrains.com \
-e retire_nginx_snippet=true \
-e retire_nginx_container=true \
playbook.yml
This repoints the real vhost at the app, removes the snippet the old 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 a week later, once the new
site has been trusted.
Rolling back
docker start nginx-menu.hopsngrains.com
ansible-playbook -l monoco -e site=hopsngrains -e tag=<previous> \
-e vhost=menu.hopsngrains.com -e retire_nginx_snippet=false playbook.yml
Then delete the app's snippet from /var/lib/caddy/conf/Caddyfile by hand, or
set vhost back to the temporary name, and reload Caddy.