Match the deploy system to hops and greatbear

menu-admin was the odd one out: it built from the working directory with a date
tag and a multi-arch manifest, deployed with `-e tag=`, and kept the Docker-era
container name. The other two apps build from `git archive HEAD` and tag with the
commit, so what runs on the host is exactly what git knows about; build only for
the architecture emily actually runs; and use one slug for the container, its
network and its data directory.

The build is now their three lines: `podman build --platform linux/arm64` from
`git archive HEAD`, tagged `:<commit>` and `:latest`, pushed with `podman push`
rather than a manifest. `deploy.sh` pushes the commit first, then refuses a dirty
tracked tree or a HEAD that is not origin/main — which is what makes the commit
tag trustworthy — takes the same `--force` and `--no-push`, and ends in the same
`-u tanshu ... -e image_tag=<commit>`. Its own lint/typecheck/test gate stays,
but behind `--checks`, so a plain deploy behaves as theirs does.

The registry credential moves into vars/default.yml and the role logs in, pulls
and logs out around it, as their roles do. The image is only pulled at deploy
time, so the unit keeps podman's default `Pull=missing` and a reboot never needs
the registry. This is the same plaintext credential the other two repos carry.

One real bug goes with it: the healthcheck gate ran before the handler that
restarts a changed unit, so a run that only changed the unit checked the stale
container. `meta: flush_handlers` before the gate is the fix the sibling repos
already made.

The container, network and data directory are all menu-<site> now. The dotted
Docker-era name and the NetworkAlias it needed are gone, so `podman logs
menu-<site>` and `systemctl --user status menu-<site>` are the same two commands
across all three apps. The Dockerfile keeps its name for the same reason: podman
reads it as readily as a Containerfile, and the siblings use that name too.
This commit is contained in:
tanshu committed 2026-10-16 11:10:59 +05:30
1 parent e68f8ab00d
commit 537a4f1e46
21 files changed
+407 -307

No files matched your search

+40 -31
View File
@@ -5,55 +5,64 @@
# ---------------------------------------------------------------------------
# Rootless Podman / Quadlet
# ---------------------------------------------------------------------------
# podman_user is derived from the connection: this play carries no become, so the
# Quadlet units belong to whoever Ansible connects as. Requires gather_facts
# (set in playbook.yml).
#
# These three duplicate caddy/vars/default.yml in the ansible-playbooks repo,
# which owns the Caddy unit these Sites sit behind. Keep them in sync.
# Derived from the connection: this play carries no become, so the Quadlet
# units belong to whoever Ansible connects as. Requires gather_facts (set in
# playbook.yml). A host that has already run caddy/playbook.yml from the
# ansible-playbooks repo has the rootless prerequisites.
podman_user: "{{ ansible_facts['user_id'] }}"
podman_data_dir: "{{ ansible_facts['user_dir'] }}/podman-data"
quadlet_dir: "{{ ansible_facts['user_dir'] }}/.config/containers/systemd"
# Which Site config inside the image to run, and which web address it serves.
# ---------------------------------------------------------------------------
# Image
# ---------------------------------------------------------------------------
registry: registry.tanshu.com
registry_username: ta-registry
registry_password: ff28a01f00c0f39315d94cd9dcb1e554968dba25676a8ea5f2be34e96a9a099f
image_name: '{{ registry }}/menu-admin'
tag: latest
# deploy.sh passes -e image_tag=<commit>; `tag` is the fallback when running the
# play by hand. Rollback = -e image_tag=<older-commit>.
image: '{{ image_name }}:{{ image_tag | default(tag) }}'
# ---------------------------------------------------------------------------
# The Site
# ---------------------------------------------------------------------------
# One image serves every Site; this picks which config inside it to run. The
# inventory name is the slug, so the container, its network and its data
# directory are all menu-<site>.
site: '{{ inventory_hostname }}'
# ---------------------------------------------------------------------------
# The app
# ---------------------------------------------------------------------------
image_name: registry.tanshu.com/menu-admin
image_tag: "{{ tag | default('latest') }}"
# The unit file is menu-<site>.container and the Podman network is
# menu-<site>_net. The container keeps the Docker-era name, dots and all, because
# scripts/provision-site-env.sh and anything an operator types look it up that
# way — but a name with dots is a DNS liability, so the Caddyfile proxies to the
# dotless NetworkAlias the unit sets beside it.
network_name: 'menu-{{ site }}'
unit_name: '{{ network_name }}.service'
container_name: 'menu-app.{{ host }}'
app_name: 'menu-{{ site }}'
container_name: '{{ app_name }}'
unit_name: '{{ container_name }}.service'
network_name: '{{ app_name }}'
# The image's own account (`node`), which keep-id maps onto podman_user.
app_uid: 1000
app_gid: 1000
container_port: 3000
# Port inside the container. Caddy proxies to it over menu-<site>_net; nothing
# is published on the host.
site_port: 3000
# podman builds an OCI image by default and drops the Containerfile's
# HEALTHCHECK instruction, so the unit declares the same check and the play gates
# on it. node is in the image and the sh -c podman wraps it in handles the quotes.
# podman builds an OCI image by default and drops the Dockerfile's HEALTHCHECK
# instruction, so the same check is declared on the Quadlet unit and the play
# gates on `podman healthcheck run`. node is in the image, and the sh -c podman
# wraps it in handles the quotes.
health_cmd: >-
node -e "fetch('http://127.0.0.1:{{ container_port }}/healthz').then(r =>
node -e "fetch('http://127.0.0.1:{{ site_port }}/healthz').then(r =>
process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
# ---------------------------------------------------------------------------
# The shared Caddy unit and its Caddyfile, both owned by the caddy playbook in
# ansible-playbooks (caddy/playbook.yml).
# Shared Caddy unit (caddy/playbook.yml in the ansible-playbooks repo)
# ---------------------------------------------------------------------------
caddy_name: caddy
caddyfile_path: '{{ podman_data_dir }}/{{ caddy_name }}/conf/Caddyfile'
caddy_snippet: |
# Appended to the shared Caddyfile by roles/caddy. The Site is served from the
# container name over menu-<site>_net; no host ports are involved.
snippet_block: |
{{ host }} {
reverse_proxy {{ network_name }}:{{ container_port }}
reverse_proxy {{ container_name }}:{{ site_port }}
}