TunaOS Architecture
A full overview of how TunaOS is made. It starts at the upstream base image and ends at a booted desktop on real hardware.
This page is the map. Each section links to the reference doc that holds the detail, and to the code that is the source of truth.
1. What TunaOS isβ
TunaOS is a family of bootc images. Each image is an OCI container that a
machine can boot. The system is immutable: /usr comes from the image, and an
update replaces the whole image, not single files.
One recipe builds every variant. A variant is one upstream base, such as
Fedora or Debian. A flavor is one desktop on that base, such as gnome or
kde. The same scripts run on all of them.
Six package managers are in use: dnf, apt, zypper, pacman, emerge
and the tunaos package factory. This is the main source of complexity in the
repository, and most of the code that looks odd exists to hold those six
paths together.
upstream base image
β
βββββββββββββ΄ββββββββββββ
β variant (9 of them) β flounder = Debian 13
βββββββββββββ¬ββββββββββββ
β
βββββββββββββ΄ββββββββββββ
β flavor / desktop β gnome kde xfce niri cosmic
βββββββββββββ¬ββββββββββββ
β
βββββββββββββ΄ββββββββββββ
β hardware layer β hwe, nvidia, asahi
βββββββββββββ¬ββββββββββββ
β
published image
ghcr.io/tuna-os/β¦
The variantsβ
| Variant | Base | Package manager |
|---|---|---|
| yellowfin | AlmaLinux Kitten 10 | dnf (el10) |
| albacore | AlmaLinux 10 | dnf (el10) |
| skipjack | CentOS Stream 10 | dnf (el10) |
| redfin | RHEL 10 | dnf (el10) |
| bonito | Fedora 44 | dnf (fedora) |
| bonito-rawhide | Fedora Rawhide | dnf (fedora) |
| grouper | Ubuntu 26.04 | apt |
| flounder | Debian 13 Trixie | apt |
| flounder-sid | Debian Sid | apt |
| marlin | Arch Linux | pacman |
| sailfin | openSUSE Tumbleweed | zypper |
| guppy | Gentoo Linux | emerge |
The authority for this table is
.github/build-config.yml.
If the two disagree, the file is right.
2. Repository mapβ
TunaOS is more than one repository. Here is what each one owns.
tuna-os/tunaOS ............ the images. Build scripts, desktop manifests,
CI matrix, install and boot tests.
tuna-os/tacklebox ......... the ISO engine, in pure Go. Registry pull,
layer unpack, EROFS, FAT ESP, ISO9660.
tuna-os/iso-builder ....... the web app. Runs tacklebox as WebAssembly
so a browser can make an ISO.
tuna-os/tunaos-packages ... the package factory. Builds RPMs and DEBs
for software the upstream repos do not ship.
tuna-os/remora ............ rebuilds user layers on the installed system.
tuna-os/corral ............ local VM and lab tooling.
tuna-os/docs .............. this documentation site.
3. How an image is madeβ
CI reads the matrix from .github/build-config.yml. It then runs a
Containerfile for the package family of that variant. There is one per
family: Containerfile.el10, Containerfile.debian, Containerfile.ubuntu,
Containerfile.arch, Containerfile.opensuse and Containerfile.gentoo.
Each Containerfile calls the numbered scripts in build_scripts/. The numbers
set the order.
build_scripts/
00-copy-files.sh ........ system_files/ into the image
01-workarounds.sh ....... per-base fixes that upstream has not landed
10-base-packages.sh ..... the packages every variant gets
20-packages.sh .......... the packages this variant gets
26-packages-post.sh ..... packages that need the ones above
40-services.sh .......... systemd units on or off
90-image-info.sh ........ /usr/share/ublue-os/image-info.json, os-release
91-arch-customizations.sh
99-cleanup.sh ........... caches out, image smaller
The desktop arrives in a later stage, not in the list above. That stage calls
build_scripts/desktop/install-desktop.sh <desktop>.
Desktop manifestsβ
install-desktop.sh reads one YAML file per desktop from
manifests/desktops/. The file holds one package list per package manager.
Two overrides exist. <desktop>-arch.yaml wins on Arch, and
<desktop>-debian.yaml wins on Debian. Ubuntu uses the plain file.
manifests/desktops/gnome.yaml
packages:
fedora: [ β¦ ] 52 packages
el10: [ β¦ ] 27 packages
apt: [ β¦ ] 12 packages
zypper: [ β¦ ] 62 packages
emerge: [ β¦ ] 2 packages
The list lengths differ a lot, and that is correct. Gentoo needs two entries, because a meta package pulls in the whole desktop. openSUSE needs sixty-two because its patterns hold the session alone.
A package count is therefore not a measure of desktop completeness. Section 7 covers the check that is.
4. How an ISO is madeβ
Two paths make an ISO. Both call the same Go packages in tacklebox, so both
produce the same media.
ββββββββββββββββββββββββββββ
β published bootc image β
ββββββββββββββ¬ββββββββββββββ
β
ββββββββββββββββββββ΄βββββββββββββββββββ
β β
ββββββββββ΄ββββββββββ βββββββββββ΄βββββββββ
β native path β β browser path β
β cmd/purebuild β β tbox.wasm β
β runs in CI β β runs in a tab β
ββββββββββ¬ββββββββββ βββββββββββ¬βββββββββ
β β
ββββββββββββββββββββ¬βββββββββββββββββββ
β
βββββββββββ΄ββββββββββ
β bootable ISO β
βββββββββββββββββββββ
What the engine doesβ
The steps are the same on both paths.
- Pull the manifest and layers from the registry.
- Unpack the layers into a tree, with OCI overlay rules.
- Graft the live overlay, and add the live user and autologin.
- Write an EROFS filesystem that holds the root.
- Write a FAT partition for EFI, with the kernel and the boot loader.
- Write an ISO9660 image with an El Torito boot record.
No step needs root. Ownership and modes live in the tree, never on a real
filesystem. internal/purefs holds the writers, and internal/oci holds the
registry and unpack code.
Why the browser path is hardβ
tbox.wasm is Go compiled to wasm32. The engine uses one linear memory.
It is 32 bits wide, so about 4 GiB is a hard ceiling. Go cannot target
Memory64, so no host setting raises it.
The engine therefore holds no file content in memory. Layer bodies stream to OPFS, and the engine reads them back in 4 MiB slices. OPFS is origin private storage, and the browser gives it to the page. The heap holds the tree and the inode table only.
See tacklebox docs/opfs-streaming-handoff.md
for the measurements behind that design.
5. The live boot chainβ
An ISO must hand control through several stages before a desktop appears. A fault at any stage looks the same to a user: a black screen.
firmware (OVMF / UEFI)
β
βΌ
El Torito boot record βββΆ EFI system partition
β
βΌ
systemd-boot βββΆ loader entry βββΆ kernel + initramfs
β
βΌ
tbox dracut modules 90tbox-live, 95tbox-root
β parse cmdline, find the media
βΌ
mount the ISO βββΆ loop mount the EROFS root
β
βΌ
overlay sysroot lowerdir = /run/rootfsbase
β
βΌ
systemd βββΆ display manager βββΆ desktop session
The kernel needs modules to mount the root: erofs, isofs, loop,
overlay, squashfs, sr_mod and cdrom. The engine lifts them out of the
image and puts them in the initramfs overlay, because a stock initramfs may
not hold them.
scripts/test-live-boot.sh in tacklebox drives this whole chain under QEMU.
It looks for a marker on the serial console and for a login prompt. That
script has a header. It lists four faults that pass every other check and
fail here.
6. Install and day twoβ
The live ISO runs an installer. After the install, the machine boots the same
image from disk, and bootc handles updates.
remora carries user choices across that boundary. The ISO holds a manifest
at /LiveOS/remora/remora.yaml with extra packages and repositories. On the
installed system, remora rebuilds those layers on the upstream base and calls
bootc switch. The customisation then survives updates.
7. The gatesβ
TunaOS has many checks. They are worth a section, because most faults found in 2026 were faults where a check passed and the artifact was still broken.
| Gate | Where | What it proves |
|---|---|---|
| Lint | .github/workflows/lint.yml | shell, YAML, JSON, actions syntax |
| Desktop contract, build | build_scripts/checks/verify-desktop-experience.sh | the desktop is usable, not only startable |
| Desktop contract, runtime | tunaos-desktop-contract.service | markers on ttyS0 after an install |
| ISO format | iso-builder @full Playwright test | size and the CD001 magic |
| Live boot | tacklebox scripts/test-live-boot.sh | the chain in section 5 reaches a login |
| Install and LUKS | scripts/iso-e2e.sh, scripts/e2e-luks-checks.sh | an encrypted install boots |
Two rules that came from real faultsβ
A session that starts is not a desktop. The contract once asked for a
shell, a session file and a display manager unit. One image met all three and
still had no file manager, no portal, no secret store and no gvfs. The contract
now asks for those too.
A package count is not a verdict. Gentoo ships two entries and gets the largest desktop of any variant. openSUSE shipped three, and got a skeleton. Only component presence separates the two.
8. Where packages come fromβ
Most packages come from the upstream repositories of the base. When upstream
does not ship something, tunaos-packages builds it.
That repository holds a package factory, Tideforge. A recipe gives native metadata for several targets: EL10 and Fedora as RPM, Debian and Ubuntu as DEB, and Arch. Output goes to a repository on Cloudflare R2, and the images add that repository.
This is the only answer when a base has no package at all. Two examples from
2026: niri has no apt package in Debian or Ubuntu, and labwc has none for
EL10.
9. Referenceβ
In the tunaOS repositoryβ
.github/build-config.ymlβ the matrix. It is the authority for every variant.manifests/desktops/β one package list per desktop, per package manager.build_scripts/README.mdβ a guide to the script tree.docs/PIPELINE.mdβ the build matrix and the stage DAG, in detail.docs/CI_SPEC.mdβ what each workflow does.docs/TESTING.mdβ how to run the tests.docs/LUKS-TPM.mdβ disk encryption.docs/SECURE-BOOT.mdβ signed boot.docs/ROLL_YOUR_OWN.mdβ make your own variant.docs/adr/β architecture decision records.
Other repositoriesβ
- tacklebox β the ISO engine (ARCHITECTURE.md)
- iso-builder β the web app
- tunaos-packages β the package factory
- remora β layer rebuild after install
- corral β local VM tooling
Upstreamβ
- bootc β boot a container image
- Universal Blue β the wider ecosystem
- EROFS β the read-only root filesystem
- dracut β the initramfs