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.

Formats and outputs
- A
.rawsuffix produces a raw disk image; a.qcow2suffix converts throughqemu-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-networkingWhat 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 PATHoverrides 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-imgon PATH, but only if you want.qcow2output;.rawneeds nothing beyond the command itself.
sudo saphira-image build \
--target uefi-saphirad \
--output /imagebuilder/saphirad-demo.qcow2 \
--size 2GThat 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.qcow2suffix converts through qemu-img at the end, while a.rawsuffix 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 2GPartitioning 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 ... OKThen 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 systemsThen 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 packagesNext, 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/dbusensure-identity: notice: 21 sysusers.d line(s) skipped; only explicitly pinned u/g identities are replayed offlineThe 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/mntThen 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 ...
doneFinally 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 MiBRead 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:
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
--sizeis the total capacity of the image or device (default 2G; K, M, G, T).--allocatecaps 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-sizenames 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-sizesets 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 8GRepository, roles and identity
--repository PATHinstalls from the given repository index (Packages.adb); the default is the host repository.--role NAME/--role-file PATHadd role layers or intention files on top of the target profile. Repeatable. Available roles includerole-dns,role-firewall,role-networkingandrole-remote-access.--hostname NAMEbakes the hostname in (defaultsaphiraon OpenRC targets,saphiradon systemd targets, so product identity runs down to the default hostname).--root-password PWsets the root password in the image.--netconf LISTwrites comma-separatedIFACE:MODE(dhcporoff) entries as/etc/saphira-network.d/10-image-IFACE.conf; empty, the default, writes nothing.--kernel-arg ARGappends toGRUB_CMDLINE_LINUX(repeatable); for what a deployment needs that the shipped default deliberately does not assume, such asconsole=ttyS0,115200for a serial console.--no-enableinstalls the profile but enables none of its services.--keep-workretains 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 MiBWhat 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.