Dosu LogoDosu Logo
Ask
Join our Discord
Organization avatar
bluefinPublic
Bluefin
Documentsbluefin
architecture
architecture
Type
External
Status
Published
Created
Jun 13, 2026
Updated
Jul 2, 2026
Updated by
Dosu Bot
Source
docs/architecture.md

Architecture#

How the Dakota live ISO is assembled and how it boots.

Build pipeline#

just iso-sd-boot dakota
  └─ just container dakota → builds localhost/dakota-installer (live/Containerfile)
  └─ iso-sd-boot (assembly) → populates VFS store, runs build-iso.sh on host

CI (build-iso.yml) — same entry point:
  └─ sudo just ... iso-sd-boot dakota → container build + VFS store + squashfs + ISO

Container: <target>-installer (live/Containerfile — 3 stages)#

StageBasePurpose
dakota-refDakota imageProvides kernel modules
initramfs-builderDebianBuilds dmsquash-live initramfs against Dakota's kernel modules
finalDakotaReceives rebuilt initramfs + live-env setup + Flatpaks

Why Debian for initramfs?
Dakota is GNOME OS / freedesktop-sdk based — no package manager, no dracut.
Building the initramfs in Debian's native environment avoids cross-distro binary grafting.
Only /tmp/initramfs.img crosses the stage boundary.

What configure-live.sh does in the final stage:

  • Sets VERSION_ID=latest in os-release (GNOME OS omits it)
  • Temporarily bind-mounts /var/home onto /home when it is safe to do so (avoids binding or unmounting when /home already resolves to /var/home or is an existing mountpoint), then creates liveuser
    (uid 1000, passwordless) so the home directory lands in the runtime-visible tree
  • Configures GDM autologin for liveuser
  • Installs and configures org.bootcinstaller.Installer Flatpak
  • Sets up live-ready.service (writes DAKOTA_LIVE_READY to serial when GDM starts)
  • Debug builds only: enables SSH, sets passwords, opens firewall port 22

ISO assembly (build-iso.sh / live/src/build-iso.sh)#

Runs on the host (not inside a container). Assembles the final ISO from the exported rootfs.

Why host-side? Host tools (xorriso, mksquashfs, mtools) avoid the overhead
of a build container and allow the justfile to control output paths directly.
xorriso available via brew at /home/linuxbrew/.linuxbrew/bin/xorriso.

Multi-arch support: Pass --arch <arch>:<boot-tar>:<squashfs> (repeatable) to produce
a single fat-ESP ISO with per-arch kernels, initramfs images, squashfs rootfs images, and both
BOOTX64.EFI + BOOTAA64.EFI. Single-arch positional-arg invocation is unchanged.
See docs/multi-arch.md for design rationale and size estimates.

ISO layout#

EFI/efi.img — FAT32 ESP: systemd-boot + kernel + initramfs
EFI/BOOT/BOOTX64.EFI — EFI fallback (Proxmox OVMF / Ventoy)
LiveOS/squashfs.img — squashfs of the full live rootfs (NVIDIA variant, with embedded VFS store)
boot/grub/loopback.cfg — Ventoy/GRUB loopback metadata
images/pxeboot/* — kernel/initramfs copies for loopback ISO boot

No GRUB2, no shim. El Torito UEFI → FAT ESP → systemd-boot → kernel + initramfs.

Boot flow#

UEFI firmware
  → El Torito (no-emulation) → FAT32 ESP
  → systemd-boot
  → kernel + initramfs (dmsquash-live)
  → scans for CDLABEL=DAKOTA_LIVE
  → mounts ISO → mounts squashfs → overlayfs (writable live env)
  → systemd → GDM autologin → GNOME session
  → org.bootcinstaller.Installer (Flatpak, auto-launched)

GPT partition layout#

The ISO uses a hybrid MBR+GPT layout.

Correct GPT type: 28732ac11ff8d211ba4b00a0c93ec93b
This is the little-endian encoding of C12A7328-F81F-11D2-BA4B-00A0C93EC93B — the
EFI System Partition GUID. UEFI firmware scanning a dd'd USB finds and boots from this.

Wrong type (old code): a2a0d0eb... — Basic Data GUID. Strict UEFI firmware won't
recognize it as bootable. If you see this, rebuild with current build-iso.sh.

Verify:

xorriso -indev output/dakota-live.iso -report_system_area plain 2>/dev/null | grep 'GPT type GUID'
# Must show: 28732ac1... (EFI System Partition OK)

Note: fdisk -l shows Disklabel type: dos on hybrid layouts — this is expected and
does NOT mean GPT is missing. gdisk, parted, and UEFI firmware see GPT correctly.

Offline image store (VFS embedded in main squashfs)#

The OCI image is baked directly into the main squashfs.img as VFS containers-storage
at /var/lib/containers/storage. The installer finds it there for offline installation
without a network pull. No separate store.squashfs.img is needed.

scripts/build-live-squashfs.sh --oci-image <ref> populates the store (used by both
just iso-sd-boot and CI). It squashes the image to a single layer via buildah commit --squash, then runs skopeo copy inside the installer container so tar-split
metadata is written in JSON format (the live ISO expects JSON; build-host
containers-storage emits binary tar-split). The VFS staging dir is then copied into the
squashfs root via cp -a (not bind-mount — see CI lessons).

Why VFS not overlay?

  • Overlay driver creates a conflicting db.sql file at first live boot
  • VFS layers are plain directories — readable without mount privileges
  • driver = "vfs" in /etc/containers/storage.conf set by configure-live.sh

skopeo /var/tmp sizing:
The squashed dakota-nvidia image has a ~9GB uncompressed layer. skopeo writes temp
blobs to /var/tmp directly (ignores TMPDIR set by fisherman). Live ISO sets
/var/tmp tmpfs to size=80% so it scales with machine RAM (16GB → 13GB available).

Embedded OCI image (VFS containers-storage)#

The squashfs embeds the Dakota OCI image as VFS containers-storage so the installer
can install offline without a network pull.

Requirements:

  • driver = "vfs" in /etc/containers/storage.conf (set by configure-live.sh)
  • skopeo copy runs inside the installer container (not the build host) to ensure
    tar-split metadata is in JSON format the live ISO expects. Build-host containers/storage
    emits binary tar-split; the installer image expects JSON.
  • fisherman scratch dir: on live ISOs /var is a small RAM overlay. fisherman detects
    tmpfs /var and uses a self-bind-mounted scratch dir on the target disk.

Installer: tuna-installer / bootc-installer#

  • Flatpak: org.bootcinstaller.Installer (stable) / org.bootcinstaller.Installer.Devel (dev)
  • Source: projectbluefin/bootc-installer (primary), tuna-os/tuna-installer (fallback)
  • Backend binary: fisherman → symlinked to /usr/local/bin/fisherman by configure-live.sh
  • Config: /etc/bootc-installer/images.json (catalog) + recipe.json (branding)
  • Flatpak sandbox: Inside the Flatpak, /etc is reserved. Host /etc is at /run/host/etc.
    Recipe passed via BOOTC_CUSTOM_RECIPE=/run/host/etc/bootc-installer/recipe.json.
  • live-iso-mode: touch /etc/bootc-installer/live-iso-mode activates live ISO mode
    in the installer.

live-ready.service#

Writes DAKOTA_LIVE_READY to the serial console after display-manager starts.
CI boot verification greps for this token. Service must use:

StandardOutput=tty
TTYPath=/dev/ttyS0 # direct serial (NOT journal+console → /dev/console)
WantedBy=multi-user.target # NOT display-manager.service (non-standard → silent failures)
After=display-manager.service # ordering only

Bundled Flatpaks#

Pre-installed into the live squashfs at build time. List in live/src/flatpaks.
The install-flatpaks.sh script uses --mount=type=cache to avoid re-downloading
on rebuilds. The cache is keyed by debug/production mode — switching busts the cache.

Tests#

Unit tests live in tests/ and run via pytest tests/ -v (gated on every PR by test.yml).

FileTestsCoverage
tests/test_luks_unlock.py52luks-unlock.py: virsh/QEMU screenshot, serial parsing, passphrase injection
tests/test_multi_arch_iso.py4build-iso.sh: --arch flag arg parsing; single-arch + multi-arch ISO integration (skipped if tools absent)

Run locally:

pip install pytest
pytest tests/ -v

Lessons#

xorriso -append_partition vs -boot_image isolinux partition_entry=gpt_basdat (2026-05)#

The old build-iso.sh used partition_entry=gpt_basdat which produces GPT type
a2a0d0eb (Basic Data). Strict UEFI firmware (bare-metal USB boot) won't recognize
this as an EFI System Partition and reports "no bootable device".

Fix: use -append_partition 2 C12A7328-F81F-11D2-BA4B-00A0C93EC93B.
Always verify with xorriso --report_system_area before shipping an ISO.

VFS vs overlay driver for containers-storage (2026-05)#

If driver = "overlay" is active in /etc/containers/storage.conf, the first bootc
operation creates a db.sql that conflicts with VFS metadata. The installer then fails
to find the embedded OCI image. configure-live.sh must explicitly set driver = "vfs".

nvidia_imgref auto-detection via bootc-installer (2026-06)#

bootc-installer v2.6.1 adds nvidia_imgref support in processor.py.
When an images.json catalog entry has nvidia_imgref, the installer auto-detects
the GPU at install time:

  • NVIDIA GPU present: installs + tracks nvidia_imgref
  • No NVIDIA GPU: installs from ISO offline store (nvidia image), but writes
    targetImgref = base imgref into the installed system — so the first bootc upgrade
    rebases to the lighter non-nvidia variant automatically.

Critical: recipe.imgref must be the BASE image (not nvidia) for
_find_nvidia_imgref_for() to match the images.json entry by imgref.
configure-live.sh uses BASE_IMGREF / NVIDIA_IMGREF separately:

  • recipe["imgref"] = BASE_IMGREF (matched by processor.py)
  • recipe["local_imgref"] = "containers-storage:{NVIDIA_IMGREF}" (offline install source)

Flatpak path bug in image.py (fixed in projectbluefin/bootc-installer#183):
_load_manifest() was hardcoded to /etc/bootc-installer/images.json. Inside a
Flatpak sandbox, host /etc is at /run/host/etc — so the live ISO's custom
images.json was never loaded, falling back to the bundled GResource which has no
nvidia_imgref. The fix applies the same /.flatpak-info detection already used by
RecipeLoader in recipe.py. This PR must land and ship in a new Flatpak release
for NVIDIA auto-detection to work end-to-end.

Storage savings from single-image store:
dakota:stable is NOT needed in the offline store — it's only a tracking ref fetched
from GHCR on the first bootc upgrade. Storing only dakota-nvidia:stable saves
~2.2 GB per ISO (~7.8 GB → ~5.6 GB).

flatpak install --bundle missing deploy/ ref in container builds (2026-06)#

flatpak install --bundle in a rootless podman container build creates the
installer-origin: remote ref but does NOT create the deploy/ ostree ref or the
active → <hash> symlink inside the branch dir. flatpak run / flatpak list require
both. The system flatpak daemon would normally create these — but it doesn't run inside
container builds.

Fix: Use ostree init + flatpak build-import-bundle + local file:// remote +
flatpak install from that remote. Then scan all branch dirs and create
active → <hash> symlinks if missing.

INSTALLER_LOCAL_REPO="/tmp/installer-local-repo"
ostree init --repo="${INSTALLER_LOCAL_REPO}" --mode=archive-z2
flatpak build-import-bundle "${INSTALLER_LOCAL_REPO}" /tmp/installer.flatpak
flatpak remote-add --system --no-gpg-verify installer-local "file://${INSTALLER_LOCAL_REPO}"
flatpak install --system --noninteractive installer-local "${INSTALLER_APP_ID}"
flatpak remote-delete --system --force installer-local

# Create missing active symlink
for _branch_dir in /var/lib/flatpak/app/${INSTALLER_APP_ID}/x86_64/*/; do
    if [[ ! -L "${_branch_dir%/}/active" ]]; then
        _hash=$(find "${_branch_dir%/}" -maxdepth 1 -mindepth 1 -type d -printf '%f\n' | head -1)
        [[ -n "${_hash}" ]] && ln -sfn "${_hash}" "${_branch_dir%/}/active"
    fi
done

bluefin-remove-installer.service whiteout files on live ISO (2026-06)#

The installed system ships bluefin-remove-installer.service which uninstalls the
installer Flatpak on first boot. On a live ISO, this service runs and tries to
flatpak uninstall, fails with "Invalid cross-device link" (squashfs-backed overlayfs),
but leaves overlayfs whiteout entries (c--------- 0,0) that hide the active and
current symlinks — making the installer invisible to flatpak run/flatpak list.

Fix: systemd drop-in ConditionPathExists=!/etc/bootc-installer/live-iso-mode.
configure-live.sh touches /etc/bootc-installer/live-iso-mode so the service skips
on live ISOs. On installed systems the file is absent — service runs normally.

Drop-in path: /etc/systemd/system/bluefin-remove-installer.service.d/live-skip.conf

[Unit]
ConditionPathExists=!/etc/bootc-installer/live-iso-mode

QEMU installed-disk boot: use qcow2, no cdrom (2026-06)#

When verifying an installed system in QEMU (not the live ISO), two things are
required for OVMF to auto-discover the virtio-blk disk without EFI NVRAM entries:

  • Install disk must be qcow2 format (convert raw → qcow2 with qemu-img convert if needed)
  • No cdrom device attached — OVMF tries cdrom first and may fail to enumerate virtio-blk

The luks-boot-qemu-installed justfile recipe follows this pattern. Boot the serial log
to confirm — GDM appearing in the log (Started gdm.service) is sufficient evidence.
The GTK window will show GNOME once GDM starts (no additional NVRAM/EFI configuration needed).

LUKS E2E CI build pipeline (2026-06)#

test-luks-install.yml originally used a 4-step manual pipeline:
podman build + build-live-squashfs.sh + build-offline-store.sh + build-iso.sh --store.
This was tied to the old superiso/overlay store pattern. After switching to the VFS
embedded store, use just iso-sd-boot directly (same as build-iso.yml):

sudo just debug=1 installer_channel=${{ matrix.installer_channel }} \
  output_dir=/var/iso-build iso-sd-boot dakota

Dev channel fallback tag: tuna-os/tuna-installer uses tag continuous-dev for
dev rolling releases, NOT latest-dev. projectbluefin/bootc-installer uses latest-dev.

Documents
Changelogs
Bluefin LTS Errata
Pin Log
2024-12-30-ublue-2024-wrapup
2025-01-01-announcements-archive
2025-02-23-february-update
2025-05-14-bluefin-f42
2025-06-23-bazaar-default
2025-07-06-fresh-bluefin-isos-and-bazaar
2025-07-11-four-years-of-ublue
2025-07-15-bold-brew
2025-07-17-ask-projectbluefin
2025-08-01-august-wallpapers
2025-08-08-bluefin-on-lfx
2025-08-29-github-oss-fund
2025-09-17-color-with-bluefin
2025-09-18-bluefin-lts-and-gdx
2025-09-23-LTS-at-indiafoss
2025-10-28-bluefin-autumn
2025-10-31-fastfetch-metrics
2025-11-15-cloud-native-milestone-15-million
2025-11-16-coreos-desktop
2025-11-24-new-just-and-bbrew
2025-11-27-bluefin-and-paleoartists
2025-11-30-report
2025-12-04-flatpak-support-in-brewfiles
2025-12-06-interview-with-michael-tunnell
2025-12-07-documentation-updates
2025-12-14-huntress-holiday-wallpapers
2025-12-15-easier-homebrew-installation-for-custom-images
2025-12-27-upcoming-homebrew-cli-changes
2025-12-30-bluefin-2025-wrapup
2025-12-31-report
2026-01-03-modernizing-custom-images
2026-01-31-report
2026-02-01-automated-reports-changelogs
2026-02-28-report
2026-03-13-bluefin-cli-mac-wsl
2026-03-27-bluefin-lts-gnome-49-50
2026-03-29-composefs-fisherman
2026-03-29-composefs-fisherman-design
2026-03-30-boot-verification
2026-03-30-boot-verification-design
2026-03-30-proportional-progress-tracking
2026-03-30-proportional-progress-tracking-design
2026-03-31-report
2026-04-02-state-of-ecosystem
2026-04-19-dakota-alpha-1
2026-04-30-report
2026-05-05-bluefin-spring
2026-05-12-bluefin-spring-2026
2026-05-13-bluefin-spring-2026-2
2026-05-14-making-our-own-fate
2026-05-15-hummingbird
2026-05-23-gradia-capture-bluefin
2026-05-28-knuckle
2026-05-31-report
2026-06-11-promotion-pr-body
AGENTS
AGENTS
AGENTS
AGENTS
AGENTS
AGENTS
Building a Custom Bluefin Image
CI_CD_GUIDE
CLAUDE
CLAUDE
CONTRIBUTING
CONTRIBUTING
CONTRIBUTING
CONTRIBUTING
CONTRIBUTING
FAQ
How can I install Double Commander on Bluefin (an immutable Linux OS), coming from Arch Linux?
How can you fix random GPU artifacts and hard freezes on an AMD Framework 16 laptop running Linux (Bluefin/Fedora)?
How do I install the Cloudflare WARP (One) client on Bluefin?
How to Reset a User's Home Directory to Defaults on Bluefin
INDEX
MAINTAINERS
MULTI_VARIANT_BUILD
PITFALLS
PULL_REQUEST_TEMPLATE
Plex Media Server
README
README
README
README
README
README
README
README
README
SECURITY
SECURITY
SECURITY
SKILL
SKILL
SKILL
SKILL
TRANSLATING
Update Process
Which Bluefin ISO is recommended for a Lenovo Yoga Pro 9i with an NVIDIA RTX 4050 for Python data science and Machine Learning with CUDA?
about-monthly-reports
action-reference
actionadon
add-package
administration
agentic-contributing
ai
analytics
architect
architecture
artwork
blog-poster.agent
bluefin-dx
bluefin-gdx
bonedigger-agent-donation
bonedigger-lifecycle
bonedigger-overview
bonedigger-templates
bonedigger-ujust
bst-overrides
build
build
buildstream
ci
ci
ci
code-of-conduct
command-line
composite-actions
consumer-guide
consumer-validation
contributing
contributors
conventional-commit.prompt
copilot-instructions
copilot-instructions
copilot-instructions
copilot-instructions
dakota-tpm2-validation
debugging
demo-mode-e2e
determinism
dinosaurs
downloads
downloads-testing
driver-versions
e2e-ci
e2e-feature-verification
encryption-matrix
factory-operations
failure-paths
feedback-loop
gstreamer-codec-validation
human-gates
images
index
index
installation
installer
introduction
knuckle
label-workflow
libpastry-integration
live-iso
local-ota
lore
lts
luks-testing
merge-queue
mission
multi-arch
music
not-bluefin
oci-assembly
oci-layers
onboarding
overview
packaging-binaries
packaging-gnome-extensions
packaging-go
packaging-rust
packaging-zig
patch-junctions
patches
pr-checklist
pr-review
press-kit
projects
pull_request_template
pull_request_template
pull_request_template
qr-phone-companion
quickstart
r2-promotion
release-qualification
remove-package
reports
reusable-workflow
skill-improvement
supply-chain
t2-mac
testing
tips
troubleshooting
ujust-recipes
update-refs
upgrade-and-migration
ux-ubuntu-provision
values
variants
vm-stack
workflow