Files
archy/docs/quadlet-compilation.md
T
archipelagoandClaude Opus 5 599787690a docs: write the three missing app-developer docs (secrets, quadlet, lifecycle)
The open-source plan flagged three references as "the real gaps for app
developers", and the docs index named them as not-yet-written. Written now,
each from the code rather than stubbed:

- secrets.md — generated_secrets/secret_env: the two halves, the four kinds
  (hex16/hex32/base64/bcrypt) and which files each writes, the idempotent
  self-healing 0600 materialisation, and the rules a developer must not break
  (no hardcoded fallbacks, one canonical name, right encoding). From
  container/secrets.rs and the manifest schema.

- quadlet-compilation.md — manifest -> .container unit: the full directive
  mapping (including Secret= by reference, never value, and Pull=never), where
  units land (~/.config/containers/systemd, systemctl --user), the
  render/write/enable/disable lifecycle with write-if-changed, and how to
  inspect one. From container/quadlet.rs, scoped accurately to the companion-UI
  path it drives today.

- container-lifecycle.md — the level-triggered 30s reconciler: desired state
  from user-stopped/user-uninstalled/manifest set, the operations table, the
  self-heal-vs-respect-a-deliberate-stop rule, and migrations-never-destroy-data.
  From prod_orchestrator.rs and boot_reconciler.rs.

Index updated to link all three under App development and the "known gap" note
removed. Every link across the docs tree resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:22:26 -04:00

122 lines
4.8 KiB
Markdown

# Manifest → Quadlet unit
How an app manifest becomes a Podman [Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)
`.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:
```ini
# 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_env` entry renders as
`Secret=<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](secrets.md).
- **`Pull=never` is 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.
## 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:
1. **render** — manifest → unit text (above).
2. **write**`tempfile + rename` so a partially-written unit is never visible to
systemd, and `write_if_changed` compares 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.
3. **enable**`daemon-reload` then start the `.service`.
4. **disable** — stop and remove.
## Inspecting a unit
Run these **as the archipelago service user** (the units are in its user bus):
```bash
# 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](container-lifecycle.md) — the reconciler that drives this
- [App Manifest Specification](app-manifest-spec.md) — the manifest fields mapped above
- [App secrets](secrets.md) — how `Secret=` references resolve
- [ADR-001: Podman over Docker](adr/001-podman-over-docker.md)