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.
58 lines
2.0 KiB
Django/Jinja
58 lines
2.0 KiB
Django/Jinja
# {{ container_name }} — rootless Podman Quadlet unit.
|
|
# Managed by Ansible (menu-admin/roles/menu-app). Do not edit by hand.
|
|
#
|
|
# This unit joins {{ network_name }}.network, which is created by
|
|
# roles/register — the same include that attaches the shared Caddy unit to it,
|
|
# so Caddy can resolve {{ container_name }} by name.
|
|
|
|
[Unit]
|
|
Description=Menu app for {{ host }} ({{ site }})
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Container]
|
|
ContainerName={{ container_name }}
|
|
Image={{ image }}
|
|
|
|
# keep-id maps the image's own account — `node`, uid/gid {{ app_uid }} — onto
|
|
# {{ podman_user }}, which keeps every file the app writes owned by
|
|
# {{ podman_user }} on the host: plain tar/rsync backups, no subuids, no
|
|
# `podman unshare`. scripts/provision-site-env.sh writes site.env with the same
|
|
# mapping rather than --user, which under rootless Podman would have produced a
|
|
# subuid-owned file the app could not read.
|
|
UserNS=keep-id:uid={{ app_uid }},gid={{ app_gid }}
|
|
|
|
Environment=MENU_ROOT=/data
|
|
Environment=MENU_SITE_CONFIG=/app/config/{{ site }}.site.json
|
|
Environment=SECURE_COOKIES=true
|
|
Environment=LOG_LEVEL=info
|
|
Environment=PORT={{ site_port }}
|
|
|
|
# Everything the app writes: data/ (originals, drafts, published, site.json,
|
|
# sections.json) and site.env. The Site's own config is baked into the image and
|
|
# read from /app.
|
|
Volume={{ host_directory }}:/data
|
|
|
|
Network={{ network_name }}.network
|
|
|
|
# No PublishPort on purpose: Caddy reaches the app over {{ network_name }}_net,
|
|
# so nothing is exposed on the host.
|
|
|
|
# podman builds an OCI image by default and drops the Dockerfile's HEALTHCHECK
|
|
# instruction, so the same check is declared here and the play gates on it with
|
|
# `podman healthcheck run`. node is in the image.
|
|
HealthCmd={{ health_cmd }}
|
|
HealthInterval=30s
|
|
HealthTimeout=5s
|
|
HealthRetries=3
|
|
HealthStartPeriod=30s
|
|
|
|
[Service]
|
|
Restart=always
|
|
# The app validates its data directory before it serves and refuses to boot on a
|
|
# fatal integrity problem, so a first start over copied data can take a moment.
|
|
TimeoutStartSec=120
|
|
|
|
[Install]
|
|
WantedBy=default.target
|