Run the Sites as rootless Podman Quadlets, and build with podman

The fleet on emily is rootless Podman with Quadlet units under
~/.config/containers/systemd and state in ~/podman-data, all behind one shared
Caddy unit. This brings the menus in line instead of leaving them the last
Docker deployment. The build moves with them: podman reads a Containerfile as
readily as a Dockerfile, so the name now says which toolchain is real.

The app is one .container and one .network file. Two details carry it.
UserNS=keep-id maps the image's own node account onto the user that owns the
units, so data/ and site.env stay owned by tanshu: plain tar backups, no
subuids. 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. And the Site registers its own network with Caddy
by writing a drop-in beside caddy.container, so Caddy never lists its upstreams
and never changes when a Site is added. The unit keeps the Docker-era container
name for the scripts and muscle memory that look it up, while the Caddyfile
proxies to a dotless NetworkAlias, because a container name with dots in it is
a liability in DNS.

The unit keeps podman's Pull=missing, so a restart or a reboot never depends on
the registry being reachable, while the play pulls explicitly at deploy time.
That is also where a missing `podman login` for the private registry now fails,
with the instruction, rather than as a unit that will not start.

copy-site-data.sh moves a Site's data across, and is not a plain rsync:
/var/lib/menu.* on monoco belongs to uid 1000 while the ssh user there is 1001,
so the read needs sudo; monoco cannot ssh to emily, so the workstation relays
the tar; and the extraction runs as tanshu on emily, which is what makes the
files land owned by the user keep-id maps the app onto. It proves the copy by
comparing a SHA-256 of the whole tree against the source and checking every
file's owner.

Not yet done: the copies themselves and the DNS move, which need a human.
This commit is contained in:
tanshu committed 2026-10-16 09:54:52 +05:30
1 parent c412181159
commit e68f8ab00d
23 files changed
+876 -389

No files matched your search

+30 -10
View File
@@ -1,15 +1,26 @@
---
# Deploys the menu app for one or more Sites. It lives here, beside the app it
# deploys, and is run from this directory:
# Deploys the menu app for one or more Sites as rootless Podman Quadlet units on
# emily, behind the shared Caddy unit that ansible-playbooks/caddy owns. It lives
# here, beside the app it deploys, and is run from this directory:
#
# make build-production TAG=<tag> (from the repo root)
# ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains
# ./deploy.sh <tag> (repo root)
# ansible-playbook --inventory=hosts playbook.yml --limit hopsngrains -e tag=<tag>
#
# One run deploys the Site: it starts the container and points the Site's web
# address at it. Nothing older is kept serving, so there is no staged rollout —
# deploying an earlier tag is the rollback.
- hosts: menus
become: true
# Connects as the container user and runs everything as that user: the Quadlet
# units belong to it, so the connection's PAM session is what makes
# `systemctl --user` work without any environment juggling.
#
# gather_facts is required: podman_user and the home directory come from the
# user_id / user_dir facts.
#
# The rootless-Podman prerequisites (installing podman, enabling lingering, the
# low-port sysctl, creating the Quadlet directory) are deliberately NOT applied
# here. They are the shared task file at
# ansible-playbooks/caddy/roles/podman/tasks/main.yaml, and a host that has
# already run ansible-playbooks/caddy/playbook.yml has them.
- name: Deploy the menu app as rootless Podman Quadlets
hosts: menus
gather_facts: true
vars_files:
- vars/default.yml
@@ -19,7 +30,16 @@
file: '{{ var_file }}'
when: var_file is defined
# This must happen before the app unit is started, not after: the generated
# unit references {{ network_name }}.network, and Quadlet fails *unit
# generation* — so `systemctl --user start` reports "unit not found" — when a
# referenced .network file does not exist yet. The same include writes the
# Caddy-side drop-in that attaches Caddy to this network.
- name: Register this Site's network with Caddy
ansible.builtin.include_tasks: '{{ playbook_dir }}/roles/register/tasks/main.yaml'
roles:
- network
# The app: the data directory tree, the Quadlet unit, start + verify.
- menu-app
# Appends this Site's block to the shared Caddyfile and reloads Caddy.
- caddy