Skip to content

RTFM · Image Creator

Build: your first image, start to finish.

One command takes a target, an output path and a size, and hands you a bootable image and an honest report of what went in. Building needs root for loop devices and mounts; planning exactly what it would do needs neither.

Saphira Linux dragon mascot

Formats and outputs

  • A .raw suffix produces a raw disk image; a .qcow2 suffix converts through qemu-img (qemu must be on PATH, otherwise the build is refused).
  • An existing output path is refused rather than overwritten, so remove it first if that is really what you want.
  • On failure the partial image is removed; a pre-existing path is never touched.
  • Scratch lives under SAPHIRA_TMPDIR (default /build/tmp), never host /tmp.
sudo saphira-image build --target uefi-systemd --size 8G \
    --output saphira-server.qcow2 --hostname srv1 \
    --role role-networking

What you need

  • Root or sudo: partitioning an image uses loop devices and mounts.
  • A reachable repository index (Packages.adb): the host repository is the default and --repository PATH overrides it.
  • Scratch space under SAPHIRA_TMPDIR (default /build/tmp): the tool keeps its workspace there, never host /tmp.
  • An output path that does not exist: existing paths are refused, never overwritten.
  • qemu-img on PATH, but only if you want .qcow2 output; .raw needs nothing beyond the command itself.
sudo saphira-image build \
  --target uefi-saphirad \
  --output /imagebuilder/saphirad-demo.qcow2 \
  --size 2G

That single command is everything. The next sections read it apart, dry-run its decisions without root, and then watch a real transcript of it.

Read the plan before you build

plan prints the exact partition layout for a target and a size: the partition types, the sector numbers and the tool's own reasoning as comments. It needs no privileges, no repository and no scratch space, so you can read it on any machine, including one that will never build an image.

# saphira-image plan: target=uefi-saphirad arch=x86_64 boot=uefi init=systemd profile=saphirad-systemd size=2G allocate=all esp=64MiB root=fill
label: gpt
# UEFI: a FAT EFI System Partition the firmware can actually read. BIOS
# rules are deliberately NOT reused here -- a bios-boot partition means
# nothing to UEFI firmware, and without a real ESP the firmware finds no
# bootloader at all.
start=2048, size=131072, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, name="esp"
start=133120, size=4059136, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="root"

The two type GUIDs are the standard EFI system and Linux filesystem codes. Sectors are 512 bytes here, so the ESP starts at sector 2048 (the 1 MiB alignment gap) and spans 131,072 sectors, which is 64 MiB, with root filling the rest of the 2 GiB. The comment lines are the tool explaining itself, which is the point of the subcommand.

Ask the report without building

report resolves a target's composed profile statically and prints the declared intentions. Nothing is built and no package is fetched; the resolved dependency closure, 61 packages for this target, only becomes visible at build time.

composed profile: /build/tmp/saphira-report.iYDC8o/profile
declared intentions: 14
  acpid
  dhcpcd
  e2fsprogs
  grub
  iputils
  nftables
  openssh
  saphira-base
  saphira-kernel-7.2.3
  saphira-network
  saphirad-release
  systemd
  tzdata
  vim

To measure the RESOLVED closure and the built image, run:
  saphira-image build --target uefi-saphirad --output /path/image.raw --report
No package is built by this subcommand.

That closing line names a flag you can copy verbatim: build accepts --report, and the image report is printed at the end of every build whether or not you pass it.

The arguments, one at a time

  • --target uefi-saphirad: which row of the matrix to build; here the full SaphiraD product image on UEFI, the profile also installed and booted on physical hardware during testing.
  • --output /imagebuilder/saphirad-demo.qcow2: where the result is written; the .qcow2 suffix converts through qemu-img at the end, while a .raw suffix writes the raw image directly.
  • --size 2G: total capacity; the ESP gets 64 MiB by default and root takes the remainder, exactly as the plan showed.

Watch a real build, start to finish

Enough reading. Here is the actual transcript of the command above, captured as it happened on the build host. It runs through five phases: partitioning, installing 61 packages, reconciling identities, setting capabilities and certificates, then enabling services and the bootloader, finishing with the report. Each phase is abridged below, and the complete transcript follows at the end.

sudo saphira-image build \
  --target uefi-saphirad \
  --output /imagebuilder/saphirad-demo.qcow2 \
  --size 2G

Partitioning first. The tool creates the raw disk and checks that nothing else is touching it:

saphira-image: creating 2G raw disk
Checking that no-one is using this disk right now ... OK

Then it writes the GPT layout that plan predicted, to the sector:

>>> Script header accepted.
>>> Created a new GPT disklabel (GUID: 27B3E65C-24F7-4A3E-97D2-AD8D81B8CF2B).
/dev/loop0p1: Created a new partition 1 of type 'EFI System' and of size 64 MiB.
/dev/loop0p2: Created a new partition 2 of type 'Linux filesystem' and of size 1.9 GiB.

and the ESP is created as FAT32, lowercase-label warning and all:

mkfs.fat 4.2 (2021-01-31)
mkfs.fat: Warning: lowercase labels might not work properly on some systems

Then the packages: 61 of them, in dependency-resolved order, through the same installer that installs a live system. The transcript runs from acpid to vim; the tail is the number that matters:

( 1/61) Installing acpid (2.0.34-r2)
( 2/61) Installing ncurses (6.5-r11)
( 3/61) Installing bash (5.3-r3)
OK: 328.5 MiB in 61 packages

Next, identity reconciliation: ensure-identity replays the explicitly pinned user and group identities into the offline image, printing every decision as it goes:

ensure-identity: group messagebus (81) created
ensure-identity: user messagebus (81:81) created
ensure-identity: shadow messagebus locked
ensure-identity: group messagebus (81) already present
ensure-identity: reconciliation complete: /build/tmp/saphira-image.qrgtJC/mnt/usr/share/saphira/accounts.d/dbus
ensure-identity: notice: 21 sysusers.d line(s) skipped; only explicitly pinned u/g identities are replayed offline

The skipping non-identity sysusers line notices you may spot in the full transcript look alarming and are not: only explicitly pinned identities are replayed offline, and the remaining accounts are created by their packages at runtime. That is expected behaviour, not a defect, and it does not hurt the image at all.

Capabilities are converged from caps.d, and 118 CA certificates are extracted, at which point the installer considers the profile complete:

ensure-caps: cap /usr/bin/ping (cap_net_raw+ep)
ensure-caps: cap /usr/bin/arping (cap_net_raw+ep)
ensure-caps: converged: /build/tmp/saphira-image.qrgtJC/mnt/usr/share/saphira/caps.d/iputils
Updating certificates in /etc/ssl/certs/: 118 added, obtained from /etc/ca-certificates.conf
saphira-install: profile installed to /build/tmp/saphira-image.qrgtJC/mnt

Then services are enabled offline, so nothing is ever started where nothing can run: symlinks under /etc record the enabled set, three mask lines keep upstream systemd-firstboot and two Saphira scripts from ever competing with Saphira's own first-boot wizard, and GRUB installs for the EFI platform:

Created symlink '/etc/systemd/system/systemd-firstboot.service' → '/dev/null'.
Created symlink '/etc/systemd/system/acpid.service' → '/dev/null'.
Created symlink '/etc/systemd/system/saphira-network.service' → '/dev/null'.
Created symlink '/etc/systemd/system/multi-user.target.wants/nftables.service' → '/usr/lib/systemd/system/nftables.service'.
Installing for x86_64-efi platform.
Installation finished. No error reported.
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-7.2.3-akadata
Adding boot menu entry for UEFI Firmware Settings ...
done

Finally the report, printed by the build and not left to your imagination:

saphira-image: image report for uefi-saphirad
  target            : uefi-saphirad (boot=uefi init=systemd profile=saphirad-systemd)
  requested size    : 2G
  files in rootfs   : 10280
  rootfs size       : 366694 KiB (358 MiB)
  image size        : 2048 MiB

Read that like a packing slip: 10,280 files, a 358 MiB rootfs, 61 packages, and the largest files listed so you can see where the megabytes went. Those numbers are measurements, not targets; nothing was removed to improve them.

The complete transcript, every line as it happened:

Full build transcript: uefi-saphirad, 2G
saphira-image: target=uefi-saphirad (boot=uefi init=systemd) profile=saphirad-systemd
saphira-image: creating 2G raw disk
Checking that no-one is using this disk right now ... OK

Disk /dev/loop0: 2 GiB, 2147483648 bytes, 4194304 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

>>> Script header accepted.
>>> Created a new GPT disklabel (GUID: 27B3E65C-24F7-4A3E-97D2-AD8D81B8CF2B).
/dev/loop0p1: Created a new partition 1 of type 'EFI System' and of size 64 MiB.
/dev/loop0p2: Created a new partition 2 of type 'Linux filesystem' and of size 1.9 GiB.
/dev/loop0p3: Done.

New situation:
Disklabel type: gpt
Disk identifier: 27B3E65C-24F7-4A3E-97D2-AD8D81B8CF2B

Device        Start     End Sectors  Size Type
/dev/loop0p1   2048  133119  131072   64M EFI System
/dev/loop0p2 133120 4192255 4059136  1.9G Linux filesystem

The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.
mkfs.fat 4.2 (2021-01-31)
mkfs.fat: Warning: lowercase labels might not work properly on some systems
( 1/61) Installing acpid (2.0.34-r2)
( 2/61) Installing ncurses (6.5-r11)
( 3/61) Installing bash (5.3-r3)
( 4/61) Installing gmp (6.3.0-r2)
( 5/61) Installing musl (1.2.6-r6)
( 6/61) Installing coreutils (9.7-r5)
( 7/61) Installing saphira-baselayout (0.1-r55)
( 8/61) Installing dhcpcd (10.5.2-r3)
( 9/61) Installing readline (8.3-r7)
(10/61) Installing zlib (1.3.1-r4)
(11/61) Installing util-linux (2.41.1-r9)
(12/61) Installing e2fsprogs (1.47.2-r4)
(13/61) Installing fuse3 (3.18.2-r1)
(14/61) Installing gettext (0.23.2-r2)
(15/61) Installing pcre2 (10.47-r4)
(16/61) Installing grep (3.12-r3)
(17/61) Installing libtasn1 (4.19.0-r2)
(18/61) Installing libaio (0.3.113-r2)
(19/61) Installing lvm2 (2.03.39-r3)
(20/61) Installing sed (4.9-r3)
(21/61) Installing xz (5.8.3-r4)
(22/61) Installing grub (2.14-r2)
(23/61) Installing libcap (2.78-r2)
(24/61) Installing mawk (1.3.4-r2)
(25/61) Installing saphira-permissions (0.1-r3)
(26/61) Installing iputils (20250605-r3)
(27/61) Installing jansson (2.14.1-r1)
(28/61) Installing libmnl (1.0.5-r2)
(29/61) Installing libnftnl (1.3.1-r2)
(30/61) Installing nftables (1.1.6-r9)
(31/61) Installing libmd (1.1.0-r2)
(32/61) Installing libxcrypt (4.4.38-r7)
(33/61) Installing linux-pam (1.7.2-r4)
(34/61) Installing saphira-base-accounts (0.1-r1)
(35/61) Installing saphira-pambase (1.0-r3)
(36/61) Installing openssh (10.6_p1-r6)
(37/61) Installing openssl (3.6.3-r3)
(38/61) Installing zstd (1.5.7-r4)
(39/61) Installing apk-tools (3.0.5-r5)
(40/61) Installing ca-certificates (20261020-r2)
(41/61) Installing saphira-base-abi (0.2-r2)
(42/61) Installing attr (2.5.2-r6)
(43/61) Installing acl (2.4.0-r3)
(44/61) Installing libbsd (0.12.2-r2)
(45/61) Installing shadow (4.17.3-r12)
(46/61) Installing saphira-base (0.1-r6)
(47/61) Installing saphira-kernel-7.2.3 (1-r13)
(48/61) Installing findutils (4.10.0-r3)
(49/61) Installing iproute2 (7.1.0-r2)
(50/61) Installing saphira-network (0.1-r13)
(51/61) Installing saphirad-release (0.2.1-r1)
(52/61) Installing gcc-libs (16.2.0-r9)
(53/61) Installing expat (2.8.2-r3)
(54/61) Installing systemd-libs (262-r6)
(55/61) Installing dbus (1.16.0-r8)
(56/61) Installing kmod (34.2-r1)
(57/61) Installing libucontext (1.5.2-r1)
(58/61) Installing udev (261.2-r8)
(59/61) Installing systemd (262-r6)
(60/61) Installing tzdata (2026a-r1)
(61/61) Installing vim (9.1.0880-r2)
OK: 328.5 MiB in 61 packages
ensure-identity: group messagebus (81) created
ensure-identity: user messagebus (81:81) created
ensure-identity: shadow messagebus locked
ensure-identity: group messagebus (81) already present
ensure-identity: reconciliation complete: /build/tmp/saphira-image.qrgtJC/mnt/usr/share/saphira/accounts.d/dbus
ensure-identity: group dhcpcd (104) already present
ensure-identity: user dhcpcd (104) already present
ensure-identity: shadow dhcpcd already locked
ensure-identity: directory /var/lib/dhcpcd ownership set to 104:104
ensure-identity: group dhcpcd (104) already present
ensure-identity: reconciliation complete: /build/tmp/saphira-image.qrgtJC/mnt/usr/share/saphira/accounts.d/dhcpcd
ensure-identity: group sshd (101) already present
ensure-identity: user sshd (101) already present
ensure-identity: shadow sshd already locked
ensure-identity: group sshd (101) already present
ensure-identity: reconciliation complete: /build/tmp/saphira-image.qrgtJC/mnt/usr/share/saphira/accounts.d/openssh
ensure-identity: reconciliation complete: /build/tmp/saphira-image.qrgtJC/mnt/usr/share/saphira/accounts.d/systemd
ensure-identity: notice: basic.conf: reserved group root 0 already present, no change
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 10: non-numeric ID '0:0' for root
ensure-identity: group nobody (65534) already present
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 14: non-numeric ID '65534:65534' for nobody
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 17: non-numeric ID '-' for adm
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 20: non-numeric ID '-' for wheel
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 21: non-numeric ID '-' for empower
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 24: non-numeric ID '-' for utmp
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 27: non-numeric ID '-' for audio
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 28: non-numeric ID '-' for cdrom
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 29: non-numeric ID '-' for clock
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 30: non-numeric ID '-' for dialout
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 31: non-numeric ID '-' for disk
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 32: non-numeric ID '-' for input
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 33: non-numeric ID '-' for kmem
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 34: non-numeric ID '-' for kvm
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 35: non-numeric ID '-' for lp
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 36: non-numeric ID '-' for render
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 37: non-numeric ID '-' for sgx
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 38: non-numeric ID '-' for tape
ensure-identity: group tty (5) already present
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 40: non-numeric ID '-' for video
ensure-identity: notice: basic.conf: skipping non-identity sysusers line: line 43: non-numeric ID '-' for users
ensure-identity: notice: dbus.conf: skipping non-identity sysusers line: line 5: non-numeric ID '-' for messagebus
ensure-identity: group systemd-coredump (144) created
ensure-identity: user systemd-coredump (144:144) created
ensure-identity: shadow systemd-coredump locked
ensure-identity: group systemd-imds (145) created
ensure-identity: user systemd-imds (145:145) created
ensure-identity: shadow systemd-imds locked
ensure-identity: group systemd-journal (131) created
ensure-identity: group systemd-network (127) created
ensure-identity: user systemd-network (127:127) created
ensure-identity: shadow systemd-network locked
ensure-identity: group systemd-oom (130) created
ensure-identity: user systemd-oom (130:130) created
ensure-identity: shadow systemd-oom locked
ensure-identity: group systemd-resolve (128) created
ensure-identity: user systemd-resolve (128:128) created
ensure-identity: shadow systemd-resolve locked
ensure-identity: group systemd-timesync (129) created
ensure-identity: user systemd-timesync (129:129) created
ensure-identity: shadow systemd-timesync locked
ensure-identity: notice: 21 sysusers.d line(s) skipped; only explicitly pinned u/g identities are replayed offline
ensure-caps: cap /usr/bin/ping (cap_net_raw+ep)
ensure-caps: cap /usr/bin/arping (cap_net_raw+ep)
ensure-caps: converged: /build/tmp/saphira-image.qrgtJC/mnt/usr/share/saphira/caps.d/iputils
Updating certificates in /etc/ssl/certs/: 118 added, obtained from /etc/ca-certificates.conf
saphira-install: profile installed to /build/tmp/saphira-image.qrgtJC/mnt
Created symlink '/etc/systemd/system/sysinit.target.wants/saphira-fhs-converge.service' → '/usr/lib/systemd/system/saphira-fhs-converge.service'.
Created symlink '/etc/systemd/system/multi-user.target.wants/saphira-firstboot.service' → '/usr/lib/systemd/system/saphira-firstboot.service'.
Created symlink '/etc/systemd/system/multi-user.target.wants/sshd.service' → '/usr/lib/systemd/system/sshd.service'.
Created symlink '/etc/systemd/system/dbus-org.freedesktop.network1.service' → '/usr/lib/systemd/system/systemd-networkd.service'.
Created symlink '/etc/systemd/system/multi-user.target.wants/systemd-networkd.service' → '/usr/lib/systemd/system/systemd-networkd.service'.
Created symlink '/etc/systemd/system/sockets.target.wants/systemd-networkd.socket' → '/usr/lib/systemd/system/systemd-networkd.socket'.
Created symlink '/etc/systemd/system/sockets.target.wants/systemd-networkd-varlink.socket' → '/usr/lib/systemd/system/systemd-networkd-varlink.socket'.
Created symlink '/etc/systemd/system/sockets.target.wants/systemd-networkd-varlink-metrics.socket' → '/usr/lib/systemd/system/systemd-networkd-varlink-metrics.socket'.
Created symlink '/etc/systemd/system/sockets.target.wants/systemd-networkd-resolve-hook.socket' → '/usr/lib/systemd/system/systemd-networkd-resolve-hook.socket'.
Created symlink '/etc/systemd/system/sysinit.target.wants/systemd-network-generator.service' → '/usr/lib/systemd/system/systemd-network-generator.service'.
Created symlink '/etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service' → '/usr/lib/systemd/system/systemd-networkd-wait-online.service'.
Created symlink '/etc/systemd/system/systemd-firstboot.service' → '/dev/null'.
Created symlink '/etc/systemd/system/acpid.service' → '/dev/null'.
Created symlink '/etc/systemd/system/saphira-network.service' → '/dev/null'.
Created symlink '/etc/systemd/system/multi-user.target.wants/nftables.service' → '/usr/lib/systemd/system/nftables.service'.
Installing for x86_64-efi platform.
Installation finished. No error reported.
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-7.2.3-akadata
Adding boot menu entry for UEFI Firmware Settings ...
done

saphira-image: image report for uefi-saphirad
  target            : uefi-saphirad (boot=uefi init=systemd profile=saphirad-systemd)
  requested size    : 2G
  files in rootfs   : 10280
  rootfs size       : 366694 KiB (358 MiB)
  image size        : 2048 MiB
  largest files     :
      13 MiB  /lib/udev/hwdb.bin
       6 MiB  /usr/lib/libcrypto.so.3
       5 MiB  /usr/lib/systemd/libsystemd-shared-262.so
       5 MiB  /lib/modules/7.2.3/kernel/drivers/gpu/drm/nouveau/nouveau.ko
       4 MiB  /usr/lib/libgettextlib-0.23.2.so
       4 MiB  /lib/modules/7.2.3/kernel/kernel/kheaders.ko
       4 MiB  /usr/lib/udev/hwdb.d/20-pci-vendor-model.hwdb
       4 MiB  /lib/udev/hwdb.d/20-pci-vendor-model.hwdb
       3 MiB  /usr/lib/libgfortran.so.5.0.0
       3 MiB  /usr/bin/vim
  note: sizes are reported for visibility, not as a target.
        No functionality is removed to improve these numbers.
    (100.00/100%)
saphira-image: /imagebuilder/saphirad-demo.qcow2 ready (target=uefi-saphirad)

Sizes that mean what they say

  • --size is the total capacity of the image or device (default 2G; K, M, G, T).
  • --allocate caps how much of that capacity the initial partition layout occupies: an absolute size (40G) or a percentage of --size (60%). The remainder stays unallocated for later growth: a 128G image with --allocate 10% is a 128G device whose partitions cover about 12.8G. Omitted, the partitions occupy the whole capacity.
  • --root-size names an exact root partition (MiB/GiB/TiB, 1 MiB aligned, at least 32 MiB). Omitted, root fills the allocation. With --allocate, root plus the fixed partitions must fit inside it; over-budget combinations are refused, nothing is resized silently.
  • --esp-size sets ESP capacity in MiB: one of 64, 128, 256, 512, 768 or 1024 (default 64). Below 64 is refused: FAT32 needs at least 65,525 data clusters, so a smaller ESP cannot be valid FAT32. Larger sizes suit Windows coexistence and UKIs; the formatter stays FAT32 at every permitted size.

Percentages resolve against usable capacity (after the 1 MiB alignment gap and the 1 MiB GPT tail reserve), so --allocate 100% means exactly what omitting the option means. The first partition starts at sector 2048; the plan is generated, never hand-written, and saphira-image plan prints it for inspection without building anything.

saphira-image plan --target uefi-systemd --size 8G

Repository, roles and identity

  • --repository PATH installs from the given repository index (Packages.adb); the default is the host repository.
  • --role NAME / --role-file PATH add role layers or intention files on top of the target profile. Repeatable. Available roles include role-dns, role-firewall, role-networking and role-remote-access.
  • --hostname NAME bakes the hostname in (default saphira on OpenRC targets, saphirad on systemd targets, so product identity runs down to the default hostname).
  • --root-password PW sets the root password in the image.
  • --netconf LIST writes comma-separated IFACE:MODE (dhcp or off) entries as /etc/saphira-network.d/10-image-IFACE.conf; empty, the default, writes nothing.
  • --kernel-arg ARG appends to GRUB_CMDLINE_LINUX (repeatable); for what a deployment needs that the shipped default deliberately does not assume, such as console=ttyS0,115200 for a serial console.
  • --no-enable installs the profile but enables none of its services.
  • --keep-work retains the scratch workspace for diagnosis when a build fails.

A profile lists intentions only; dependency closure belongs to APK metadata and is never duplicated into an image definition. Role layers add intentions on top: a role is profile data, not a meta-package, so no new package name is invented and the package manager still owns the closure.

Behind the numbers

Every build ends with the image report: target, file and rootfs sizes, and the largest payload files. Sizes are reported for visibility, never pursued; no functionality is removed to improve them, and the report prints that itself.

The qcow2 is what makes the numbers interesting. The demonstration image measures 2 GiB virtual and 383 MiB on disk, because QCOW2 stores data in clusters and lets the empty remainder of the virtual disk cost nothing until files actually claim it. A raw image claims its whole capacity on disk unless it is kept sparse; that trade in disk usage is why the suffix matters, and nothing else in the build depends on it.

image: /imagebuilder/saphirad-demo.qcow2
file format: qcow2
virtual size: 2 GiB (2147483648 bytes)
disk size: 383 MiB

What is verified and what is not: the physical install (install-to-disk onto real UEFI hardware) and the first boot were performed and observed; this qcow2 file was built and inspected but never booted in a hypervisor here, and this walkthrough does not claim otherwise. Nothing in the build is VM-only, but treat a hypervisor boot as your own experiment rather than documented fact.

Where this leaves you: an image file on disk, a report you can grep, and a plan you can re-check at any time. The machine's first boot is documented next in First boot; putting the same system on real hardware is Install to disk.