5.1 KiB
Manifest → Quadlet unit
How an app manifest becomes a Podman Quadlet
.container unit that systemd owns, where the unit lands, and how to inspect one.
Source of truth: core/archipelago/src/container/quadlet.rs.
Why Quadlet
Containers used to be fire-and-forget tokio::spawn blocks. If the daemon
crashed mid-spawn or the kernel reaped a parent cgroup, the container vanished
from podman ps and only a manual podman run brought it back. Quadlet removes
that whole class of failure: the unit lives on disk, systemd owns
start/restart, and archipelago is just the provisioner. This is the path that
runs the companion UI containers today (archy-bitcoin-ui, archy-lnd-ui,
archy-electrs-ui), and the validated path being flipped to default for apps.
What gets generated
Quadlet::from_manifest(manifest, name) translates a manifest into a unit, and
render() produces the file. Every unit carries a header making clear it is not
hand-edited:
# Generated by archipelago. DO NOT EDIT.
# Edits are overwritten on the next reconcile.
[Unit]
Description=<app description>
After=network-online.target
Wants=network-online.target
Requires=<dependency>.service # one per declared dependency
After=<dependency>.service
[Container]
ContainerName=<name>
Image=<image ref>
Pull=never # image must be present locally already
Network=<host | pasta | slirp4netns | bridge name>
User=<uid> # when the manifest pins one
DropCapability=ALL # security default
AddCapability=<cap> # only capabilities the manifest opts into
PublishPort=<bind>:<host>:<container>/<proto>
Environment=<KEY>=<value> # non-secret env only
Secret=<secret_name>,type=env,target=<KEY> # secrets by REFERENCE, never value
Volume=<source>:<target><opts>
ReadOnly=true # when security.readonly_root
NoNewPrivileges=true # when security.no_new_privileges
HealthCmd=<cmd> # from the health_check block
[Service]
TimeoutStartSec=0
Restart=<always | on-failure> # from the restart policy
RestartSec=10 # 10s backoff caps a crash loop
[Install]
WantedBy=default.target
Two things to note in that mapping:
- Secrets go in by reference, never by value. A
secret_enventry renders asSecret=<name>,type=env,target=<KEY>, so podman injects the value at run time from the node's secret store. The plaintext never appears in the unit file. See App secrets. Pull=neveris deliberate. The provisioner does not pull images from here; the image must already be local (pre-pulled or built). A missing image surfaces immediately instead of retrying silently behind systemd's restart loop.PublishPortis dropped entirely underNetwork=host. Podman rejects the combination and the container crash-loops on exit 125, so declared ports are omitted rather than rendered. With host networking the container is already on the host's ports; a manifest that declares both is not an error, the mapping is just silently unnecessary.
Where units land
Rootless, per-user, under the archipelago service user (uid 1000, with linger enabled so the units run without an active login):
~/.config/containers/systemd/<name>.container
Quadlet's systemd generator translates <name>.container into a
<name>.service unit at daemon-reload time. Everything is systemctl --user
— the system bus is never touched from this path.
Lifecycle: render → write → enable → disable
The module does four things and nothing else:
- render — manifest → unit text (above).
- write —
tempfile + renameso a partially-written unit is never visible to systemd, andwrite_if_changedcompares bytes first: if the rendered unit matches what is on disk, nothing is touched — no daemon-reload, no restart cascade. This is what makes a reconcile tick cheap and non-disruptive. - enable —
daemon-reloadthen start the.service. - disable — stop and remove.
Inspecting a unit
Run these as the archipelago service user (the units are in its user bus):
# the generated unit
cat ~/.config/containers/systemd/archy-bitcoin-ui.container
# what systemd made of it
systemctl --user cat archy-bitcoin-ui.service
systemctl --user status archy-bitcoin-ui.service
journalctl --user -u archy-bitcoin-ui.service
# after editing a unit by hand for debugging (it will be overwritten on reconcile)
systemctl --user daemon-reload
Because the unit is regenerated on every reconcile, the way to change a
container's shape is to change its manifest (and, for a catalog-covered app,
regenerate and re-sign the catalog), never to edit the .container file — the
DO NOT EDIT header is literal.
Related
- Container lifecycle — the reconciler that drives this
- App Manifest Specification — the manifest fields mapped above
- App secrets — how
Secret=references resolve - ADR-001: Podman over Docker