| 1 |
# Image composition |
| 2 |
|
| 3 |
How Alloy is built and delivered. bootc + Containerfile, source on `git.sr.ht`, CI on `builds.sr.ht`, container image on `quay.io`. Companion to [STACK.md](STACK.md). |
| 4 |
|
| 5 |
## Landscape |
| 6 |
|
| 7 |
Silverblue and its atomic siblings don't have a mutable root filesystem. The OS is an immutable commit, a tree of files that swaps atomically at boot. Users can't `dnf install` into a running system; they either layer packages locally (rebuilding their image) or pull a pre-built image. |
| 8 |
|
| 9 |
For a distro builder, "image composition" is the question of how you produce that immutable commit and how users receive it. |
| 10 |
|
| 11 |
## Options considered |
| 12 |
|
| 13 |
### rpm-ostree compose (traditional) |
| 14 |
|
| 15 |
Classical Silverblue build. Treefile in YAML declares packages, kernel arguments, systemd presets. `rpm-ostree compose tree` produces an ostree commit, pushed to an ostree repository. Users pull via `rpm-ostree upgrade`. |
| 16 |
|
| 17 |
**Cost:** you host an ostree repository (real infrastructure: nginx serving `/var/ostree` or equivalent). Legacy-shaped; Fedora itself is migrating away. |
| 18 |
|
| 19 |
### bootc + Containerfile (modern, where Fedora is going) |
| 20 |
|
| 21 |
The OS *is* an OCI container image. Distributed via any OCI registry. Users pull with `bootc switch <registry-url>` and reboot. Build is `podman build` from a Containerfile, the same as building any Docker image. |
| 22 |
|
| 23 |
**Cost:** pre-1.0, occasional breaking changes as bootc matures. Alloy accepts drift maintenance the same way it does for nushell config and userChrome.css. |
| 24 |
|
| 25 |
### ublue-downstream (rejected) |
| 26 |
|
| 27 |
Universal Blue's convention is exactly the bootc + Containerfile pattern above, using `FROM ghcr.io/ublue-os/main`. Battle-tested. But being downstream of ublue means inheriting ublue's opinions and coupling to their release cadence, which [STACK.md's Base section](STACK.md#base) explicitly rejects. |
| 28 |
|
| 29 |
**The nuance:** Alloy uses the ublue-style *convention* (Containerfile, OCI registry, CI-driven build) *without* using ublue's base image. `FROM quay.io/fedora/fedora-bootc:43`, not `FROM ghcr.io/ublue-os/main`. That's what "alongside ublue, not downstream" resolves to in practice. |
| 30 |
|
| 31 |
### "Install Silverblue and run our script" (non-option) |
| 32 |
|
| 33 |
Zero build infrastructure. But users get a mutated Silverblue with an Alloy veneer, not shipped as "Alloy." Manifesto commitment breaks. Rejected. |
| 34 |
|
| 35 |
## Pick: bootc + Containerfile |
| 36 |
|
| 37 |
The tool choice. Rationale summarized: aligns with where Fedora is going; build tooling is `podman build`; distribution is any OCI registry (free); Containerfile is a single-file OS specification. |
| 38 |
|
| 39 |
## Delivery stack |
| 40 |
|
| 41 |
### Source: `git.sr.ht/~maxmj/alloy` |
| 42 |
|
| 43 |
Primary remote. Alloy is source-hosted on sr.ht for the same reasons the rest of Make Creative's public work is: independence from big-tech-monopoly infrastructure at the layer where users install software. `srht` is a canonical remote for Alloy, not a backup. |
| 44 |
|
| 45 |
### CI: `builds.sr.ht` (disabled during local-iteration phase) |
| 46 |
|
| 47 |
sr.ht ships Fedora build VMs with `dnf`, `podman`, `buildah`, `skopeo` preinstalled. When ready, the build file at `.builds/alloy-image.yml` triggers on every push. |
| 48 |
|
| 49 |
**Current state:** the build file lives at `builds.disabled/alloy-image.yml`, parked outside the `.builds/` path sr.ht auto-runs, so no CI resources are consumed while the Containerfile is being iterated locally. Rename the directory back to `.builds/` and push to re-enable. |
| 50 |
|
| 51 |
### Registry: `quay.io` (deferred until distribution starts) |
| 52 |
|
| 53 |
Red Hat's container registry is the intended destination once Alloy has an image worth distributing. Free for public images, ecosystem-coherent with the `quay.io/fedora/fedora-bootc:43` base. |
| 54 |
|
| 55 |
**Current state:** no registry provisioned. `.builds/alloy-image.yml` runs a **build + lint smoke test** only, validating the Containerfile on every push, without publishing anywhere. When the image is worth distributing, the CI file grows back `sign` and `push` tasks and `quay.io/alloy/alloy` gets provisioned with a robot account. |
| 56 |
|
| 57 |
Rejected alternatives (for when distribution starts): ghcr.io (source-registry ecosystem mismatch), Docker Hub (pull rate limits, no advantage), self-hosted (real infra commitment, not worth v0 operational load; revisit v2+). |
| 58 |
|
| 59 |
### Signing: cosign (deferred alongside distribution) |
| 60 |
|
| 61 |
Once a registry is publishing images, cosign signs them as a CI task. Private key stored as an sr.ht secret; public key published in the Alloy repo. Users verify via `bootc switch --signature-source=<url>`. Nothing to set up until distribution starts. |
| 62 |
|
| 63 |
## Layer structure |
| 64 |
|
| 65 |
The Containerfile at the repo root (see `Containerfile`) sketches: |
| 66 |
|
| 67 |
1. **Base:** `FROM quay.io/fedora/fedora-bootc:43`. |
| 68 |
2. **Third-party repos:** Tailscale, any COPRs Alloy depends on for packages not in Fedora main. |
| 69 |
3. **Package additions:** the full Alloy stack from [STACK.md](STACK.md): compositor, bar, launcher, notifications, terminal, editor, shell, viewers, utilities, continuity daemons, fonts, themes. |
| 70 |
4. **Package removals:** stock Silverblue desktop pieces Alloy replaces (gnome-shell, gdm; the latter gated on the greeter pick). |
| 71 |
5. **Config tree:** the tree at `etc/skel/.config/*` and `etc/skel/.mozilla/*` (new-user defaults, including the Firefox first-launch profile seed), `etc/*` (system-wide, including `etc/firefox/policies/policies.json`), and `usr/lib64/firefox/*` (Firefox autoconfig + `mozilla.cfg` default prefs) in the repo maps 1:1 into the image. |
| 72 |
6. **Systemd presets:** which services are enabled by default (syncthing off by default, gammastep off until enrolled, alloy-hinged conditionally on FW12, etc.). |
| 73 |
7. **Branding:** os-release, plymouth splash. |
| 74 |
8. **Validation:** `bootc container lint` runs at build. |
| 75 |
|
| 76 |
The base browser (Firefox) ships as an RPM baked into the image: one code path, no first-boot delay, and enterprise policies (`/etc/firefox/policies/policies.json`) take effect immediately. The `flatpak` client is included so users can pull ungoogled-chromium and other Flathub-only apps on demand post-install; no Flatpaks are provisioned at build or first-boot time. |
| 77 |
|
| 78 |
## Update cadence |
| 79 |
|
| 80 |
The sr.ht build triggers on: |
| 81 |
|
| 82 |
- **Push to `main`**: Alloy commits. |
| 83 |
- **Weekly cron**: picks up upstream Fedora base image updates. sr.ht doesn't have webhook receivers for external triggers, so cron is the pragmatic answer. |
| 84 |
|
| 85 |
Users receive updates via `bootc upgrade`: no push notifications, no forced restart. Alloy's update UX is the standard bootc UX. |
| 86 |
|
| 87 |
## Fedora version tracking |
| 88 |
|
| 89 |
Pinned explicitly, currently `:43`. Bumps to `:44` and beyond are a deliberate act: Alloy tests the new base, updates the Containerfile, ships. Not automatic. This is the "correctness over deadline" pattern applied to base tracking. |
| 90 |
|
| 91 |
Note that `build/alloy-0.0.yaml` remains bootc-image-builder's fedora-42 package set, because bib ships no fedora-43 definition. That is deliberate and documented in the file's own header; it is not drift. |
| 92 |
|
| 93 |
## Open questions |
| 94 |
|
| 95 |
- [ ] Verify which Alloy packages are in Fedora main vs. need COPRs. Candidates that may need COPRs: `satty`, `wl-screenrec` (depending on Fedora version). Audit at v0 packaging time. |
| 96 |
- [ ] `bootc-image-builder` for ISO generation. First-time-user path is `bootc install` from a live environment; the ISO is what makes that a smooth experience. Verify the ISO builder handles Alloy's specific package set. |
| 97 |
- [x] **Containerfile stage policy: multi-stage only where a non-Fedora Rust binary needs to ship.** Currently that's one stage (`rust-build` for wl-screenrec); everything else comes from RPM in the runtime stage. Do not add stages preemptively. |
| 98 |
- [x] sr.ht account: **`~maxmj`** for v0. LLC-owned `~makecreative` remains the long-term target; migration if/when the project grows. |
| 99 |
- [x] **Publish `:latest`, `:<fedora-version>`, and `:<fedora-version>-YYYYMMDD` tags.** Currently `:latest`, `:43`, `:43-YYYYMMDD`. `:latest` for casual users, the bare version for version-pinned users, the dated tag for reproducibility. Wired up when distribution starts. |
| 100 |
|