Installer ISO
An installer ISO is built locally from a DeskOS image with bootc-image-builder's
anaconda-iso type (older builder releases called the same output iso). It
embeds the image and installs it offline with the kickstart below.
Requirements: Linux with rootful Podman and about 15 GB free.
Build
1. Choose the image reference
The installer runs bootc switch to the reference the ISO is built from, so that
reference is where installed machines update from.
-
For the CentOS Stream 10 image: use
quay.io/deskos/deskos-core-centos-stream-10:latest, or the image you built locally. architecture.md carries the digest:stableserves. -
For your own image, tag it with the reference they will update from and push it before handing out the ISO; a
localhost/tag leaves them without an update source:deskosctl render --workstation deskos-core-centos-stream-10 --output ctxsudo podman build -t registry.example.com/deskos/core:stable ctxsudo podman push registry.example.com/deskos/core:stable
2. Write the kickstart
Write config.toml:
# bootc-image-builder enables only the Network, Payloads and Storage modules,
# which leaves the Language/Keyboard and Time & Date spokes out of the summary.
[customizations.installer.modules]
enable = [
"org.fedoraproject.Anaconda.Modules.Localization",
"org.fedoraproject.Anaconda.Modules.Timezone",
]
[customizations.installer.kickstart]
contents = """
# Graphical Anaconda, and nothing else is answered.
graphical
rootpw --lock
reboot
"""
[!WARNING] The installer asks for language, keyboard, timezone, network (optional), the disk and LUKS, and then erases the selected disk. It no longer wipes every disk it sees; read the destination screen.
rootpw --lockand nouser: GNOME Initial Setup creates the first account, so there is no root password to guess.- No
clearpartand noautopart: the disk, the partitioning and LUKS belong to whoever installs. - No
lang,keyboard,timezone,networkor hostname: the installer asks, and the ISO carries the image so the install works offline. TheLocalizationandTimezonemodules must be enabled for those spokes to appear. - No
xconfig --startxonboot: Anaconda sets the default target itself (ConfigureSystemdDefaultTargetTask):graphical.targetwhen a package providesservice(graphical-login), which gdm does, andmulti-user.targetotherwise. DeskOS ships gdm, so the line does nothing here, and the install without it reached GNOME Initial Setup. Keep it only for a base with no graphical login. graphicalruns the graphical UI. Without--non-interactive, a complete kickstart installs without a click and an incomplete one waits on the summary hub instead of aborting. RHEL 10.2's Anaconda aborts a graphical non-interactive install at the progress screen (User interaction required on standalone spoke ProgressSpoke), so do not add the flag back. With no display, usetext.
What you can change
The kickstart is Anaconda's, and config.toml is bootc-image-builder's. These are
the options DeskOS uses, and the ones to avoid:
| Goal | Option | Notes |
|---|---|---|
| Show the language, keyboard and time zone screens | [customizations.installer.modules] enable with Localization and Timezone | bootc-image-builder enables only Network, Payloads and Storage, so without this those spokes are missing from the summary |
| Answer language, keyboard or time zone instead of asking | lang, keyboard, timezone in the kickstart | the ISO carries the image, so this works offline |
| Set the hostname or bring up the network | network in the kickstart | optional; a wired install works without it |
| Leave no root password | rootpw --lock | with no user line, GNOME Initial Setup creates the first account |
| Boot into a graphical session after install | nothing to do | Anaconda picks graphical.target when gdm is installed and multi-user.target otherwise; xconfig --startxonboot is the historical way to force the first |
| Graphical or text installer | graphical or text | text is the choice on a host with no display |
| Partitioning, disk selection | clearpart and autopart | avoid: the person installing chooses the disk and LUKS, and clearpart --all erases every disk the installer sees |
| Encryption | autopart --encrypted --passphrase=... | avoid: the passphrase would sit in the ISO in clear text |
| Extra packages or files | %post | avoid: that is image content, and it belongs in the image; secrets in a kickstart are visible to anyone with the ISO |
| Unattended install | --non-interactive | avoid on RHEL 10: measured abort at the progress screen (User interaction required on standalone spoke ProgressSpoke) |
Without a config.toml, bootc-image-builder writes its own kickstart, and that
one is not interactive. Its osbuild-base.ks and osbuild.ks, read from an ISO
built without a configuration on 2026-10-29, answer language, keyboard and time
zone, lock root, run clearpart --all and autopart --nohome --type=plain, and
finish with reboot --eject.
That ISO was installed in a QEMU VM with UEFI and one empty disk: it asked nothing, wiped the disk, installed, rebooted and reached GNOME Initial Setup. It works, and it is destructive: any other disk the installer sees goes with it.
The configuration above exists to remove those lines. With it, Anaconda asks for the disk, the partitioning and LUKS. Dropping it does not make the installer friendlier, it makes it unattended and destructive.
A private registry
bootc-image-builder records the reference the ISO was built from and nothing
else: no credentials. The ISO embeds the image, so installing does not contact
the registry; the first bootc upgrade does.
The installed machine reads /etc/ostree/auth.json, in the containers auth format
and mode 0600. /run/ostree/auth.json works for one boot:
{ "auths": { "registry.example.com": { "auth": "<base64 of user:token>" } } }
printf '%s' 'user:token' | base64 produces the value, and the token needs pull
only. For ghcr.io the user is a GitHub account and the token a personal access
token with read:packages.
Deliver the file from provisioning: configuration management, cloud-init, or a
kickstart %post. RHEL's own guide warns that a kickstart carrying credentials
exposes them to anyone with the ISO, so keep the token scoped to pull and rotate
it. Never bake it into the image.
3. Build the ISO
mkdir -p output
sudo podman run --rm --privileged --pull=missing \
--security-opt label=type:unconfined_t \
-v ./output:/output -v ./config.toml:/config.toml:ro \
-v /var/lib/containers/storage:/var/lib/containers/storage \
quay.io/centos-bootc/bootc-image-builder:latest \
build --type anaconda-iso --config /config.toml \
--chown "$(id -u):$(id -g)" registry.example.com/deskos/core:stable
The ISO is output/bootiso/install.iso.
RHEL
This repository does not build RHEL. The same steps apply in a downstream
organization's repository, on an entitled host logged in to registry.redhat.io,
with registry.redhat.io/rhel10/bootc-image-builder instead of the CentOS
builder.
[!IMPORTANT] RHEL ISOs must never be published: keep them inside the organization that builds them.
Validation status
The CentOS Stream 10 preview ISO at https://download.deskos.org/ was installed
and booted in KVM: DeskOS splash, then GNOME Initial Setup over the DeskOS
wallpaper. It is a preview built from a published image, not from the current
main; verify its published checksum.
Not tested here: bare metal, BIOS boot, RHEL ISOs (downstream), and updates
through the bootc switch reference.
Not tested in CI: no check installs the ISO; it is built, signed and published,
and only a person has installed one. That gap reached a published ISO: a downstream
organization's RHEL ISO carried graphical --non-interactive, which RHEL 10.2's
Anaconda aborts. The kickstart here does not use the flag. A test-only ISO was
installed unattended in KVM and reached GNOME Initial Setup; that is a manual run.
Storage is interactive, so this ISO cannot finish an install without a person: a QEMU check can assert that the installer reaches the hub with an editable Installation Destination, but not that the install completes. Automated install tests use the test-only ISO in tests/installer: the same image with a kickstart that answers storage, locale and network. It is never published; do not put those answers into the shipped ISO.