Experimental mkosi backend
July 28, 2026 ยท View on GitHub
This is an experimental Debian/mkosi implementation of the dstack guest OS.
It is not yet a replacement for the release Yocto backend. Its acceptance
target is functional parity with the Yocto image, not merely release archive
compatibility. parity.json is the machine-checked inventory used by a build.
The backend builds the same dstack services plus the pinned Yocto component set: Linux 6.18, NVIDIA 595.58.03 (open modules, userspace, firmware, Fabric Manager and NSCQ), nvattest 2026.06.09 with the OCSP-freshness patch, OpenZFS 2.4.0, Sysbox 0.6.7, NVIDIA Container Toolkit, nerdctl, CNI plugins and stargz-snapshotter 0.18.2.
The kernel tracks the same series as the production Yocto backend
(PREFERRED_VERSION_linux-yocto in meta-dstack), which tests/acceptance.sh
enforces. That is what keeps the component set patch-free: ZFS 2.4.0 declares
Linux-Maximum: 6.18 and the NVIDIA 595.58.03 open modules build against this
series unmodified, so neither carries an out-of-tree compatibility patch that
production does not also carry.
Reproducibility model
versions.env pins the stable kernel, Rust and Go archives by SHA-256 and
selects an immutable Debian snapshot. GOTOOLCHAIN=local keeps the go command
from replacing the pinned compiler when a module's go.mod names a newer
release. All components
are compiled by mkosi.build inside mkosi's build-package overlay; host
Rust/Go/GCC binaries are never used. C/C++ compilers, linkers, headers and
build systems come from that snapshot, while the Rust and Go distributions are
installed under /opt/dstack-toolchains after checksum verification. A mkosi
tools tree built from the same snapshot supplies image-construction tools.
Cargo.lock, --locked, a fixed SOURCE_DATE_EPOCH, normalized file mtimes,
fixed kernel build identity and mkosi's deterministic image construction close
the remaining inputs. DSTACK_SKIP_RUST=1 exists only for rootfs/kernel
development and does not produce a functional guest.
The custom kernel starts from x86_64_defconfig, then applies the reviewed
kernel.config fragment. This is the practical upstream equivalent of Yocto's
linux-yocto-tiny plus explicit features: disabling arbitrary defconfig
symbols without resolving Kconfig dependencies would be less auditable. Both
the TDX DMA patch and the ACPI BadAML/SystemMemory sandbox patch are reused
verbatim from meta-dstack; patch fuzz is forbidden. The final .config is
checked before compilation.
Build and acceptance
Host compilation and image-assembly toolchains are not required. mkosi 26
creates the pinned Debian build overlay containing all component compilers and
headers. Its minimized misc tools-tree profile plus explicit packages supplies
lddtree, squashfs, dm-verity, disk, archive and UKI tools. The repository's
dstack-mr and the pinned nitro-tpm-pcr-compute revision are compiled by
mkosi.build, exported only for mkosi.postoutput, and removed from the guest.
The host needs mkosi's own dependencies and root privileges (or a working user
namespace), but no Rust, Go, C or C++ compiler.
The container path in repro-build/ reduces that to Docker plus the privileges
mkosi needs for loop devices, device-mapper and mounts. It pins the last layer
the backend itself does not: mkosi and the host tools it drives.
make os-image-mkosi # single production build
make os-repro-check-mkosi # build twice, compare byte for byte
./os/mkosi/repro-build/repro-build.sh -o /path/to/build-dir
The native interface remains available when those host tools are present:
./os/mkosi/build.sh lint
./os/mkosi/build.sh image "$PWD/os/mkosi/build"
./os/build.sh --backend mkosi --build-dir "$PWD/os/mkosi/build"
./os/mkosi/build.sh repro-check "$PWD/os/mkosi/repro"
# QEMU smoke-test the assembled UKI disk (host OVMF path is distro-specific)
qemu-system-x86_64 -machine q35 -m 2G -nographic \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
-drive if=virtio,format=raw,file=os/mkosi/build/out/prod/dstack-0.6.0/disk.raw
image reuses a component-output cache, which is what makes local iteration
affordable. --no-cache forces the cold path; the release workflow and the
container path above both pass it, and repro-check never consults the cache at
all. DSTACK_COMPONENT_CACHE=0 in the environment changes the default for
callers that cannot pass a flag, such as os/build.sh.
DSTACK_DEV_CACHE_DIR="$HOME/.cache/dstack/mkosi-dev" \
./os/mkosi/build.sh image "$PWD/os/mkosi/build-dev"
./os/mkosi/build.sh --no-cache image "$PWD/os/mkosi/build"
A cached build also skips the two release tarballs, which are roughly a minute
of gzip over artifacts that already sit unpacked beside them. disk.raw, the
measurements and metadata.json are still produced, so QEMU smoke-testing and
measurement inspection are unaffected; --archive asks a cached build for the
tarballs anyway. A cold build always archives, and repro-check archives
unconditionally because the tarballs are what it compares.
The cache covers dstack Rust, image tools, the container stack, Sysbox, nvattest, the
kernel build tree, NVIDIA, ZFS and both OVMF variants. Its key conservatively
includes the inputs, tools, packages and component dependencies declared by
each descriptor in components/<name>/<name>.sh, plus architecture, flavor and
SOURCE_DATE_EPOCH. For linked worktrees, build.sh records the Git-owned and
untracked source path inventory on the host, while file contents are hashed in
the mkosi build root; Git metadata is never mounted into the sandbox.
build-components.sh is intentionally only the ordered component list.
Component install trees are merged with strict non-directory conflict
detection. A cold build never calculates, reads or writes component cache keys.
Release artifacts, Debian rootfs, dm-verity data and measurements are never
cached.
The cache is stored in mkosi's BuildDirectory=, which is the only mount a
build script gets that is both writable and preserved between runs. It must not
live on a BuildSources= mount: BuildSourcesEphemeral=yes gives every source
an overlay whose upper layer is a temporary directory, so anything written
there is discarded when the build script exits.
mkosi's Incremental=, CacheDirectory= and BuildDirectory= cover
whole-image/rootfs and persistent-work-directory reuse; none of them provides
independently keyed output trees for components or rejects file collisions when
those trees are installed. The small component layer supplies only those two
missing policies, on top of BuildDirectory= for storage. Source download,
build, cache-key inputs and output ownership remain together under
components/<name>/; production builds bypass the layer's archive cache
completely.
On a 16-job development host, a clean production work directory takes about
27 minutes with warm package downloads; allow 30--45 minutes with cold network
caches. repro-check performs two such builds sequentially, deliberately with
different job counts as well as different build paths, so output that depends
on parallelism is caught rather than reproduced identically twice.
Acceptance is currently split. build.sh lint enforces the static contract
and runs on every change touching os/; it is a source-level check, not a
statement about a built image. The image-level criteria -- a UKI disk booting
on x86_64 QEMU, /proc/config.gz carrying the checked TDX/SNP, TPM, ACPI,
dm-verity/crypt, virtio, container and hardening options, dstack services
enabled, and two clean builds comparing byte-for-byte -- are verified by
repro-check and by hand today; only the byte-for-byte comparison is
automated, behind a manual workflow dispatch. The backend exports
artifact-manifest schema v1 and delegates final assembly to
os/image/assemble.sh, exactly like Yocto. Its output contains the same
dstack-<version>/ directory (dstack-dev-<version>/ for the dev flavor,
matching Yocto), bare-metal and UKI tarballs, partitioned combined
squashfs/dm-verity image, metadata, measurements, checksums, kernel, initramfs,
OVMF and UKI. Debian supplies the base userspace while the parity checker
requires the Yocto-visible binaries, services, configuration, kernel modules
and production/development separation before assembly is allowed to proceed.
The firmware is not Debian's generic OVMF: components/ovmf/ovmf-build.sh builds the same
EDK2 stable-202502 revision and pre202505 TDX measurement layout selected by
the Yocto recipe. A generic OVMF cannot be substituted because dstack-mr
would produce invalid or unparseable TDX measurement material.
Native mkosi boundary
The distribution snapshot, build-only packages, guest packages, profiles,
source mounts, source-date epoch, tools tree, package cleanup, file removal,
systemd presets, tmpfiles, service masks and the build/postinstall/finalize/
postoutput/clean lifecycle are mkosi-native. The native tar output is replaced
in mkosi.postoutput by the identically named Yocto-compatible archive, so no
unrelated mkosi rootfs artifact escapes the staging directory.
Only four project-specific mechanisms remain:
make-release-artifacts.shandos/image/assemble.shimplement the existing combined squashfs/dm-verity layout, initramfs command-line protocol, measurements and archive member contract. mkosi's repart/UKI formats would change that external interface.- The development-only component cache provides independently keyed output
trees. mkosi's
Incremental=andBuildSourcesEphemeral=buildcachecache a whole image or a mutable build tree, not isolated component install outputs. Only the keying and collision policies are ours; the storage is mkosi'sBuildDirectory=. merge-component-trees.pyrejects conflicting component-owned rootfs paths; mkosi's normal tree overlays intentionally use last-writer-wins semantics.normalize-skeleton-modes.shmaps regular skeleton files to Git's two portable mode classes (0644 or 0755). mkosi correctly preserves source modes, but Git worktrees on shared hosts can add group-write bits that are not represented in the Git index and would otherwise change the rootfs.
The tiny postinstall hook is also retained because mkosi's native MachineId=
supports a UUID, random, or uninitialized, while the Yocto contract requires
an existing but empty /etc/machine-id. Rust and Go distributions are pinned by
version and SHA-256 inside the mkosi build overlay because Debian trixie's Rust
package is too old for this workspace; they never come from the host.