# 2. Building your image Alloy publishes source and never publishes a binary image. You build the installer ISO yourself, on a machine you already own. That is not an interim state waiting on infrastructure. It is the distribution model: what you boot is the thing you configured, and Make Creative hosts no registry, signs no images, and redistributes nobody's binaries. ## What you need - **A Linux machine with podman.** The build runs rootful. - **Disk.** The image, the container cache, and the ISO working directory together want tens of gigabytes free. - **Time.** Around fifteen minutes on a current laptop with a fast mirror, most of it in three roughly equal parts: compiling the console and the terminal, pulling and installing around 600 packages, and assembling the ISO. A slow connection moves the second part and dominates the rest. There is no path to an Alloy ISO from macOS or Windows. The build needs Linux with podman, and nothing emulates that cheaply enough to recommend. ## Get the source git clone https://makenot.work/git/max/alloy cd alloy ## Build the ISO build/build-iso.sh That builds the bootc image (`localhost/alloy:local`), then wraps it into an installer ISO under `output/`. The ISO carries the image twice: once as the live system you boot, and once as the source that gets installed to the disk. Useful flags: | Flag | What it does | |---|---| | `--skip-build` | Reuse the image already in container storage | | `--fast` | Reuse the image and compress cheaply. For iterating on the boot chain | | `--skip-source` | Omit the install source. The ISO boots but cannot install | | `--update-target host:5000/alloy:43` | Bake an update registry into the medium (development builds only) | Without `--update-target`, machines installed from the medium never fetch updates. That is the intended state for anything but a development build. ## Write it to a USB stick Any of the usual tools work on the ISO in `output/`. If you would rather have the write checked for you, both build scripts take the same two flags: build/build-iso.sh --write-only --write /dev/sdX It refuses partitions, refuses anything with a mounted filesystem, demands an explicit device path plus a typed confirmation, and verifies the result with `cmp` before claiming success. ## Building a disk image instead `build/build-image.sh` produces raw or qcow2 disk images through bootc-image-builder, which is what you want for a VM: build/build-image.sh --type qcow2 It refuses to build ISOs on purpose. Every ISO type that tool offers composes an Anaconda installer, and an Anaconda install of Alloy leaves a machine with no account and a locked root. The Alloy installer ISO comes from `build/build-iso.sh` and nowhere else. ## Baking in a key and a hostname A medium can carry the public key and the machine name the install will use, which is what makes a headless install possible: you reach the installer over SSH rather than typing at a console the machine does not have. build/build-iso.sh \ --build-arg ALLOY_SSH_KEY="$(cat ~/.ssh/id_ed25519.pub)" \ --build-arg ALLOY_HOSTNAME=workshop `ALLOY_SSH_KEY` takes the key itself and not a path to it, because the build container cannot see your filesystem. The build refuses anything that is not a public key, and refuses outright if it looks like a private one. The key lands at `/usr/lib/alloy/authorized_keys` on the medium and does two jobs from there. It is the credential for the `installer` account, which exists only on the live medium and drops you straight into `alloy install`. And the installer's account screen offers it back to you as the key for the machine being installed, so the credential you connect with is the one that ends up on the finished system unless you replace it. `alloy image` is the same thing with a form around it: it asks for a profile, a browser, languages, a database, what to trim, a hostname and a path to a public key, and runs the script above with what you chose. Chapter 3 covers the install itself, and what the SSH posture means once the machine is up. ## Changing what goes in The Containerfile at the repo root is the whole package list, and it is meant to be read and edited. Adding a package is a line in the right group; removing one is deleting a line. Two cautions: - The build asserts a number of things it depends on (that `satty`, `grim`, `slurp`, `jq` and `notify-send` exist for the screenshot script, that the fontconfig picks resolve, that the polkit rules name actions that exist). Removing a package can trip an assertion, which is the build telling you the removal has consequences somewhere else. - Anything you add that Alloy ships should get a line in `crates/alloy/credits.toml`, which is what the installer's credits page reads. It is hand-curated on purpose, so nothing adds itself. ## Carrying a database `DB` decides whether the image ships PostgreSQL, and it defaults to `none`. A stock mint carries no database and nothing about it changes by this dial existing. podman build --build-arg DB=postgres16 ... `postgres16` installs `postgresql16` and `postgresql16-server` from `updates`: three packages, 9 MiB of download and 38 MiB installed. It is deliberately not Fedora's own `postgresql-server`, which is 18.4. The build host runs Sando's scratch cluster to test MNW against what production runs, and production is on 16, so a cluster two majors ahead would test a database nobody deploys. The arm puts the binaries on `PATH` and stops. No `systemctl enable`, no `initdb`: a cluster is state belonging to the machine that runs it rather than to the image. ## When the build cannot reach the fonts Alloy cuts its two faces during the build from a pinned upstream base, and the base is fetched from raw.githubusercontent.com, which rate-limits by IP. A busy network or a shared address can earn an HTTP 429. The fetch retries five times before it gives up, and curl retries a 429 like any other transient answer, so what gets through to you is a host that stayed unwilling. One build asks that host three times: the two faces the image cuts, and a third that the terminal's own build script cuts while `cargo install shop` runs. The seed below covers the first two. It does not cover the terminal's, which is the fetch that failed in August, because the terminal's build script names its own cache directory and nothing in the image build can point it elsewhere. What covers all three is a mirror, and the build takes one by default: `QUASI_TYPE_MIRROR` is set to `https://makenot.work/static/bases`, where the pinned files are served under the sha256 each pin already carries. The tool tries the mirror first and falls back to the pinned upstream url, and it checks the bytes against that digest either way, so a mirror can serve the pinned file or nothing. A build with no access to makenot.work is a slower first build and nothing more; to switch it off, build/build-iso.sh --build-arg QUASI_TYPE_MIRROR= The build log says which way it went. Warming the cargo target directory still covers the terminal's fetch on a host that has built once. To seed the two the image cuts, put the four files in `build/base-cache/` (its README names them and says where to copy them from). They are carried into the build and used in place of a download. To make the cut refuse to reach out at all: build/build-iso.sh --build-arg QUASI_BASES=sealed If the seed is short of a file the pins name, that fails before the tool is built, listing what is missing, rather than at the cut. It seals the cut and nothing else: the build still clones two repositories, resolves crates, installs packages, and cuts the terminal's face. Keeping the bases outside the repo works too, with `podman build -v