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.
This commit is contained in:
tanshu committed 2026-09-22 23:18:18 +05:30
1 parent e64f65642f
commit 75b6492493
17 files changed
+417 -1067

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
---
- name: Pull the app image
docker_image:
name: '{{ image_name }}'
tag: '{{ image_tag }}'
source: pull
force_source: yes
- name: Ensure the app root exists
file:
path: '{{ app_root }}'
state: directory
owner: '{{ app_uid }}'
group: '{{ app_gid }}'
mode: '0750'
- name: Ensure the data directory exists
file:
path: '{{ app_root }}/data'
state: directory
owner: '{{ app_uid }}'
group: '{{ app_gid }}'
mode: '0750'
- name: Look for the untracked site.env
stat:
path: '{{ app_root }}/site.env'
register: menu_site_env
- name: Warn when the admin has no password yet
debug:
msg: >-
{{ app_root }}/site.env is missing, so the admin is disabled: the public
menu still serves. Run scripts/provision-site-env.sh {{ app_root }} on
this host, then re-run this playbook.
when: not menu_site_env.stat.exists
- name: Run the menu app
docker_container:
name: '{{ docker_container }}'
image: '{{ image_name }}:{{ image_tag }}'
state: started
restart_policy: unless-stopped
user: '{{ app_uid }}:{{ app_gid }}'
env:
MENU_ROOT: /data
MENU_SITE_CONFIG: '/app/config/{{ site }}.site.json'
SECURE_COOKIES: 'true'
LOG_LEVEL: info
PORT: '3000'
volumes:
- '{{ app_root }}:/data'
networks:
- name: '{{ docker_network }}'
- name: Keep site.env readable only by the app user
file:
path: '{{ app_root }}/site.env'
owner: '{{ app_uid }}'
group: '{{ app_gid }}'
mode: '0600'
when: menu_site_env.stat.exists
- name: Wait for the app to report healthy
# The image's own HEALTHCHECK fetches /healthz inside the container. Polling
# that is deliberate: a container name resolves only on the Docker network, so
# the host cannot fetch http://{{ docker_container }}:3000/healthz.
docker_container_info:
name: '{{ docker_container }}'
register: menu_health
changed_when: false
retries: 20
delay: 5
until: >-
menu_health.container is defined and
menu_health.container.State.Health is defined and
menu_health.container.State.Health.Status == 'healthy'
- name: Fail when the app never becomes healthy
fail:
msg: >-
{{ docker_container }} did not report healthy within 100s. Check
`docker logs {{ docker_container }}`.
when: menu_health.container.State.Health.Status | default('unknown') != 'healthy'
- name: Check the data directory for integrity problems
# Reads the same directory the container writes, as the same user, so a
# permissions mistake shows up here rather than at 8pm service.
command:
argv:
- docker
- run
- --rm
- --user
- '{{ app_uid }}:{{ app_gid }}'
- -e
- MENU_ROOT=/data
- -e
- 'MENU_SITE_CONFIG=/app/config/{{ site }}.site.json'
- --entrypoint
- node
- '{{ image_name }}:{{ image_tag }}'
- src/cli/verify.ts
register: menu_verify
changed_when: false
failed_when: false
- name: Report integrity problems
debug:
msg: '{{ menu_verify.stdout_lines | default([]) }}'
when: menu_verify.rc != 0