Documents
README
README
Type
External
Status
Published
Created
Jun 13, 2026
Updated
Jun 13, 2026
Source
View

bootc-installer#

A GTK 4 / Libadwaita Flatpak installer for Project Bluefin and other Universal Blue bootc images.


bootc-installer is a guided graphical installer for bootc container-native OS images. It handles everything from disk partitioning and encryption setup to post-install personalisation — including importing your files and settings from an existing Windows installation.

It supports two distinct boot stacks:

StackImagesBootloaderRoot FSLayout
systemd-bootDakotasystemd-boot + UKIbtrfs + composefs2-partition (EFI + root)
GRUB2Bluefin, Bluefin-LTS, BazziteGRUB2XFS or btrfs3-partition (EFI + /boot + root)

Features#

Install pipeline#

The fisherman Go backend executes a 9-step pipeline entirely from a JSON recipe:

  1. Partition — layout depends on the target boot stack:
    • systemd-boot (Dakota): 2-partition GPT — EFI (1 GiB FAT32) + root. systemd-boot reads the FAT32 ESP directly; no separate /boot partition is needed. The root partition is tagged with the architecture-specific GPT type GUID so systemd-boot can auto-discover it.
    • GRUB2 (Bluefin, Bluefin-LTS): 3-partition GPT — EFI (FAT32) + /boot (ext4) + root. The separate ext4 /boot is required because GRUB's built-in XFS driver cannot read modern XFS features (nrext64, exchange, rmapbt), and bootupctl needs to find the /boot UUID from a raw block device rather than a LUKS mapper device.
  2. Format — EFI (mkfs.fat -F32) and, for GRUB2 images only, /boot (mkfs.ext4)
  3. LUKS encryption (optional) — cryptsetup luksFormat + luksOpen
  4. Format rootmkfs.xfs or mkfs.btrfs (with optional named subvolumes)
  5. Mount — everything assembled under /mnt/fisherman-target
  6. bootc installpodman run --privileged bootc install to-filesystem --skip-finalize writes the OS into the mounted root. --skip-finalize keeps the target writable so post-install steps can still write files.
  7. Post-install — hostname, Flatpak copy, Bluetooth/WiFi persistence, audio device naming, OEM detection, cache warming
  8. Windows data migration (optional) — imports documents, photos, music, bookmarks, fonts, and wallpapers from an existing Windows partition
  9. Finalizefstrim → remount read-only → fsfreeze/fsthaw. This replicates what bootc install finalize would do (it is currently a no-op upstream), ensuring a clean filesystem state before reboot.

Scratch space note: fisherman uses /var/fisherman-tmp (disk-backed, bind-mounted to /var/tmp) as scratch space for OCI blob downloads. /run is a tmpfs capped at ~50% RAM and is too small for large images — do not redirect scratch there.

Dakota / systemd-boot images#

Dakota is the next-generation Project Bluefin variant built on a modern sealed-image stack. The installer handles it as a first-class target with a different code path from GRUB2-based images:

FeatureDakota
Bootloadersystemd-boot + UKI (Unified Kernel Image)
Deployment backendcomposefs (--composefs-backend)
Root filesystembtrfs
Partition layout2-partition GPT: EFI (1 GiB FAT32) + root
GPT auto-discoveryRoot partition tagged with the x86-64 Linux root GUID so systemd-boot finds it without explicit configuration
User creationNot required — dakota ships a first-boot user-setup flow
Recipe fields"bootloader": "systemd", "composeFsBackend": true

Bluefin / Bluefin-LTS continue to use the GRUB2 + 3-partition + XFS/ext4 stack. The installer auto-selects the correct path based on bootloader and composeFsBackend in the recipe — no manual steps needed.

Encryption#

ModeDescription
noneUnencrypted install
luks-passphraseLUKS2 with user-supplied passphrase
tpm2-luksLUKS2 auto-unlocked by TPM2 at boot (no passphrase prompt)
tpm2-luks-passphraseTPM2 primary + passphrase fallback. A recovery key is shown on screen and must be acknowledged before proceeding.

Instant first boot#

These run automatically during every install — no user action required:

FeatureWhat it does
Bluetooth persistenceCopies /var/lib/bluetooth into the installed OS so previously-paired devices reconnect immediately on first boot
WiFi persistenceCopies NetworkManager .nmconnection files so saved networks reconnect automatically
Audio device namingInstalls WirePlumber rules that rename ugly ALSA identifiers (e.g. alsa_output.pci-0000_00_1f.3.analog-stereo) to human-readable names, and hides S/PDIF and Pro Audio sinks
Live audio fixApplies the same audio naming rules to the live session immediately, so headphones work before you even reboot
OEM detectionDetects ASUS, Framework, and TUXEDO hardware and queues the appropriate first-boot brew packages
Cache warmingPre-generates font, icon, pixbuf, GIO, ldconfig, man-db, and Flatpak caches so the first boot feels instant rather than spending 30+ seconds regenerating them
Print servicesEnables cups-browsed, avahi-daemon, and ipp-usb so USB printers and AirPrint work out of the box

Windows data migration (Slurp)#

When an existing Windows partition is detected, the installer offers to import your data. A scan runs asynchronously and shows per-user category checkboxes with size estimates:

CategoryWhat's imported
DocumentsFiles from Documents, Desktop, Downloads
PhotosFiles from Pictures
MusicFiles from Music
BookmarksChrome/Edge browser bookmarks
FontsUser-installed fonts from AppData\Local\Microsoft\Windows\Fonts
WallpaperCurrent and recent Windows wallpapers — always imported as a silent easter egg even if the slurp step is skipped

A RAM budget warning appears if the selected categories would exceed available memory. Wallpaper thumbnails for the GNOME wallpaper picker are pre-generated during install.

Phone companion#

During installation, the installer starts a local HTTPS server (port 8443, self-signed cert) and displays a QR code. Scanning it with your phone opens a page where you can fill in account details and preferences from your phone instead of typing on the installer screen. Configuration is submitted as JSON and fed directly into the recipe.

Video playback during install#

The progress screen plays a branded AV1/VP9 video during the install. Distributions can provide their own video at /etc/bootc-installer/install-video.webm. The installer validates GStreamer codec availability before attempting playback and falls back gracefully to a static progress display if the required codecs are missing.

Offline / live ISO install#

When the installer detects it is running on a live ISO (via /etc/bootc-installer/live-iso-mode), it switches to offline mode: the pre-embedded OCI image in the ISO's VFS containers-storage is passed to fisherman via additionalImageStores so no network pull is required.


Installing#

Production#

curl -Lo installer.flatpak \
  https://github.com/projectbluefin/bootc-installer/releases/download/latest-stable/org.bootcinstaller.Installer.flatpak \
  && sudo flatpak uninstall -y org.bootcinstaller.Installer org.bootcos.Installer 2>/dev/null; sudo flatpak install --bundle -y installer.flatpak

Devel (latest dev branch build)#

curl -Lo installer-devel.flatpak \
  https://github.com/projectbluefin/bootc-installer/releases/download/latest-dev/org.bootcinstaller.Installer.Devel.flatpak \
  && sudo flatpak uninstall -y org.bootcinstaller.Installer.Devel 2>/dev/null; sudo flatpak install --bundle -y installer-devel.flatpak

Recipe Format#

The installer drives the fisherman backend with a JSON recipe file. The wizard generates one automatically, but you can write one by hand for automation or liveISO customisation.

Minimal recipe (Bluefin / Bluefin-LTS, XFS, GRUB2, no encryption)#

{
  "disk": "/dev/sda",
  "filesystem": "xfs",
  "btrfsSubvolumes": false,
  "encryption": { "type": "none" },
  "image": "ghcr.io/projectbluefin/bootcos:latest",
  "targetImgref": "ghcr.io/projectbluefin/bootcos:latest",
  "selinuxDisabled": false,
  "unifiedStorage": true,
  "composeFsBackend": false,
  "bootloader": "grub2",
  "hostname": "bootcos",
  "flatpaks": [],
  "user": {
    "username": "james",
    "fullname": "James",
    "password": "hunter2",
    "groups": ["wheel"]
  }
}

Dakota (systemd-boot + composefs + btrfs)#

{
  "disk": "/dev/nvme0n1",
  "filesystem": "btrfs",
  "btrfsSubvolumes": false,
  "encryption": { "type": "none" },
  "image": "ghcr.io/projectbluefin/dakota:latest",
  "targetImgref": "ghcr.io/projectbluefin/dakota:latest",
  "selinuxDisabled": false,
  "unifiedStorage": true,
  "composeFsBackend": true,
  "bootloader": "systemd",
  "hostname": "bootcos",
  "flatpaks": [],
  "user": { "username": "", "fullname": "", "password": "", "groups": [] }
}

bootloader: "systemd" selects the 2-partition layout (EFI + root) and enables GPT auto-discovery root retagging. composeFsBackend: true passes --composefs-backend to bootc. User creation is skipped for Dakota because it ships its own first-boot setup flow.

Btrfs + TPM2/LUKS encryption (Bluefin-LTS)#

{
  "disk": "/dev/nvme0n1",
  "filesystem": "btrfs",
  "btrfsSubvolumes": true,
  "encryption": { "type": "tpm2-luks" },
  "image": "ghcr.io/projectbluefin/bootcos:latest",
  "targetImgref": "ghcr.io/projectbluefin/bootcos:latest",
  "selinuxDisabled": false,
  "unifiedStorage": true,
  "composeFsBackend": false,
  "bootloader": "grub2",
  "hostname": "bootcos",
  "flatpaks": ["org.mozilla.firefox", "org.gnome.Console"],
  "user": {
    "username": "james",
    "fullname": "James",
    "password": "hunter2",
    "groups": ["wheel"]
  }
}

LUKS with passphrase fallback + TPM2#

{
  "disk": "/dev/sda",
  "filesystem": "xfs",
  "btrfsSubvolumes": false,
  "encryption": {
    "type": "tpm2-luks-passphrase",
    "passphrase": "my-recovery-passphrase"
  },
  "image": "ghcr.io/projectbluefin/bootcos:latest",
  "targetImgref": "ghcr.io/projectbluefin/bootcos:latest",
  "selinuxDisabled": false,
  "unifiedStorage": true,
  "composeFsBackend": false,
  "bootloader": "grub2",
  "hostname": "bootcos",
  "flatpaks": [],
  "user": { "username": "", "fullname": "", "password": "", "groups": [] }
}

Recipe field reference#

FieldTypeDescription
diskstringBlock device to partition and install to (e.g. "/dev/sda")
filesystemstringRoot filesystem: "xfs" or "btrfs"
btrfsSubvolumesboolCreate @, @home, @snapshots subvolumes (btrfs only)
encryption.typestring"none", "luks-passphrase", "tpm2-luks", or "tpm2-luks-passphrase"
encryption.passphrasestringRequired for luks-passphrase and tpm2-luks-passphrase
imagestringSource OCI image to install (pulled by podman)
targetImgrefstringUpdate-tracking ref written into the deployed OS (usually same as image)
selinuxDisabledboolPass --disable-selinux to bootc (needed for cross-distro installs)
unifiedStorageboolPass --experimental-unified-storage to bootc (default: true)
composeFsBackendboolPass --composefs-backend to bootc (required for composefs-native images like Dakota)
bootloaderstring"grub2" (default) or "systemd" (for systemd-boot images)
hostnamestringHostname written into the installed OS
flatpaksarrayFlatpak app IDs to copy from the live system into the target
user.usernamestringUsername to create; leave empty to skip user creation
user.fullnamestringFull display name
user.passwordstringPlain-text password (hashed during install)
user.groupsarrayAdditional groups (e.g. ["wheel", "docker"])

Autoinstall (unattended)#

Pass a recipe directly to skip the wizard entirely:

flatpak run org.bootcinstaller.Installer --autoinstall /path/to/recipe.json

This is the primary mechanism for liveISO pre-configuration (see below).


Image / Distro Customisation#

Any image or distro shipping this installer — whether on a liveISO, as part of a bootc image (Bluefin, Bazzite, etc.), or as a custom appliance — can pre-configure both the image catalog and the default recipe by dropping files into /etc/bootc-installer/ in their OS tree. The installer reads these at runtime from the host filesystem.

Override pathScope
/etc/bootc-installer/images.jsonImage catalog — replaces the bundled catalog entirely
/etc/bootc-installer/recipe.jsonSys-recipe — sets default values merged with the wizard output
$XDG_CONFIG_HOME/bootc-installer/images.jsonPer-user catalog override (dev/testing)

Customising the image catalog#

Override images.json to show only your own images and hide the default catalog:

/etc/bootc-installer/images.json

{
  "default_image": "ghcr.io/my-org/my-image:stable",
  "fallback_flatpaks": [],
  "images": [
    {
      "name": "My Distro",
      "icon": "/usr/share/pixmaps/my-distro.svg",
      "needs_user_creation": true,
      "children": [
        {
          "name": "Stable",
          "imgref": "ghcr.io/my-org/my-image:stable",
          "desc": "The stable release"
        },
        {
          "name": "Nightly",
          "imgref": "ghcr.io/my-org/my-image:nightly",
          "desc": "Latest nightly build"
        }
      ]
    }
  ]
}

Bluefin example — only show Bluefin variants, pre-select the LTS:

{
  "default_image": "ghcr.io/ublue-os/bluefin:lts",
  "fallback_flatpaks": [],
  "images": [
    {
      "name": "Bluefin",
      "icon": "/usr/share/pixmaps/bluefin.png",
      "flatpaks": "https://raw.githubusercontent.com/projectbluefin/common/refs/heads/main/system_files/bluefin/usr/share/ublue-os/homebrew/system-flatpaks.Brewfile",
      "needs_user_creation": true,
      "children": [
        {
          "name": "Stable",
          "imgref": "ghcr.io/ublue-os/bluefin:stable"
        },
        {
          "name": "LTS",
          "imgref": "ghcr.io/ublue-os/bluefin:lts"
        },
        {
          "name": "Dakota (experimental)",
          "imgref": "ghcr.io/projectbluefin/dakota:latest",
          "composefs": true,
          "bootloader": "systemd",
          "filesystem": "btrfs",
          "needs_user_creation": false
        }
      ]
    }
  ]
}

Setting recipe defaults (sys-recipe)#

Drop a partial recipe at /etc/bootc-installer/recipe.json. It is merged with the wizard output — the user can still change anything, but these values are used as defaults for fields they don't touch:

/etc/bootc-installer/recipe.json

{
  "hostname": "bluefin",
  "selinuxDisabled": false,
  "unifiedStorage": true,
  "flatpaks": [
    "org.mozilla.firefox",
    "org.gnome.Console"
  ]
}

Bazzite example — force btrfs + TPM2 LUKS as the default, pre-fill hostname:

{
  "hostname": "bazzite",
  "filesystem": "btrfs",
  "btrfsSubvolumes": true,
  "encryption": { "type": "tpm2-luks" },
  "unifiedStorage": true
}

Unattended / autoinstall#

For a fully hands-free install (e.g. a liveISO that installs automatically), pass --autoinstall with a complete recipe — the wizard is skipped entirely:

flatpak run org.bootcinstaller.Installer --autoinstall /etc/bootc-installer/autoinstall.json

Example systemd unit to trigger this on boot:

/usr/lib/systemd/system/bootc-installer-auto.service

[Unit]
Description=Unattended bootc install
After=graphical.target

[Service]
ExecStart=flatpak run org.bootcinstaller.Installer --autoinstall /etc/bootc-installer/autoinstall.json

Contributing Images#

The installer's image catalog is defined in fisherman/data/images.json. Adding a new image means adding an entry to that file. The structure is a recursive tree of groups and leaves:

{
  "name": "My Distro",
  "subtitle": "Optional subtitle",
  "icon": "resource:///org/bootcinstaller/Installer/images/my-distro.svg",
  "flatpaks": ["org.mozilla.firefox", "org.gnome.Console"],
  "needs_user_creation": true,
  "children": [
    {
      "name": "Stable",
      "imgref": "ghcr.io/my-org/my-image:latest",
      "desc": "Optional description shown as tooltip"
    },
    {
      "name": "Composefs Edition",
      "imgref": "ghcr.io/my-org/my-image-composefs:latest",
      "composefs": true,
      "bootloader": "systemd",
      "filesystem": "btrfs",
      "needs_user_creation": false
    }
  ]
}

Inheritable group fields (children inherit the nearest ancestor's value):

FieldDescription
flatpaksFlatpak list URL or app ID array
iconresource:///…/images/name.svg, absolute path, or XDG icon name
needs_user_creationWhether the installer shows the user creation step (default: false)
composefsEnable composefs deployment backend
bootloader"grub2" or "systemd"
filesystem"xfs" or "btrfs"

Drop your SVG/PNG into fisherman/data/images/ and add it to bootc_installer/bootc-installer.gresource.xml.

PRs to add new images, icons, or flatpak lists are very welcome!


Building#

flatpak run org.flatpak.Builder --force-clean --user --install _build flatpak/org.bootcinstaller.Installer.json
flatpak run org.bootcinstaller.Installer

Meson (development)#

meson setup build
ninja -C build
sudo ninja -C build install
bootc-installer

Demo / preview loop#

For day-to-day UI work, use the repo launcher:

./run-dev.sh

It runs the local build with BOOTC_DEMO=1, so the full wizard can be walked
without launching fisherman or touching a disk.

To jump directly to an individual screen during local iteration, set
BOOTC_PREVIEW_SCREEN before launching:

BOOTC_PREVIEW_SCREEN=confirm ./run-dev.sh
BOOTC_PREVIEW_SCREEN=progress ./run-dev.sh

progress starts the demo install sequence automatically when BOOTC_DEMO=1
is enabled.

Dependencies#

  • meson, ninja
  • libadwaita-1-dev
  • gettext, desktop-file-utils
  • libgnome-desktop-4-dev
  • python3-requests
  • Go ≥ 1.22 (for fisherman)

Testing and release qualification#

Software-only validation that is already wired into this repo:

./QUALIFY_SOFTWARE.sh

Optional local install/boot smoke coverage also exists in tests/integration/test_e2e_install.py for root + QEMU environments:

sudo FISHERMAN_BIN=/path/to/fisherman pytest tests/integration/test_e2e_install.py -v -s
sudo FISHERMAN_BIN=/path/to/fisherman BOOT_VERIFY=1 pytest tests/integration/test_e2e_install.py -v -s

Real release qualification still requires destructive installs on lab hardware for TPM2, physical boot prompts, recovery-key/passphrase fallback, Windows slurp, and offline ISO paths. Use the repo runbook in .github/CI_CD_GUIDE.md to separate what can be verified now from what remains hardware-only.

README | Dosu