# 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= After=network-online.target Wants=network-online.target Requires=.service # one per declared dependency After=.service [Container] ContainerName= Image= Pull=never # image must be present locally already Network= User= # when the manifest pins one DropCapability=ALL # security default AddCapability= # only capabilities the manifest opts into PublishPort=::/ Environment== # non-secret env only Secret=,type=env,target= # secrets by REFERENCE, never value Volume=: ReadOnly=true # when security.readonly_root NoNewPrivileges=true # when security.no_new_privileges HealthCmd= # from the health_check block [Service] TimeoutStartSec=0 Restart= # 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=,type=env,target=`, 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/.container ``` Quadlet's systemd generator translates `.container` into a `.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)