Build the dstack guest OS
July 13, 2026 · View on GitHub
This guide builds the bootable dstack guest-OS release artifacts from source.
It is for OS developers, release maintainers, and operators who want a custom
image. You do not need to build an image for normal self-hosted onboarding:
dstackup install downloads and verifies a published guest-OS release by
default.
What the build produces
The default prod build produces:
- a bare-metal/CVM bundle for Intel TDX and, when the SEV firmware artifact is available, AMD SEV-SNP;
- a UKI disk-image bundle for the GCP confidential-VM boot path;
- dm-verity rootfs data, launch-measurement material, checksums, and the unified
digest.txtOS identity.
Yocto is currently the only implemented OS backend. Backend-independent rootfs
payload, artifact contract, measurement, and release packaging live outside
os/yocto/; see ../os/README.md.
Prerequisites
Use an x86-64 Linux host with:
- Git;
- Docker Engine, usable by the current user;
- outbound HTTPS access for Git, Yocto source archives, and Rust crates;
- substantial free disk space for Yocto downloads, work directories, and shared-state cache.
TEE hardware is not required to build the image. It is required only when you boot and attest the resulting image on the corresponding platform.
Check the basics before starting:
docker version
git --version
df -h .
Quick build
From a fresh checkout:
git clone https://github.com/Dstack-TEE/dstack.git
cd dstack
make os-image
make os-image initializes only the eight Yocto dependency submodules and
runs one complete production image build in the pinned Ubuntu builder
container. It is equivalent to:
git submodule update --init --depth 1 -- \
os/yocto/deps/bitbake \
os/yocto/deps/openembedded-core \
os/yocto/deps/meta-yocto \
os/yocto/deps/meta-confidential-compute \
os/yocto/deps/meta-virtualization \
os/yocto/deps/meta-openembedded \
os/yocto/deps/meta-rust-bin \
os/yocto/deps/meta-security
cd os/yocto/repro-build
./repro-build.sh -n
The first build downloads and compiles the complete Yocto toolchain and guest
userspace, so it is much slower than an incremental rebuild. The -n option
means “build once”; it does not skip BitBake or image assembly.
Outputs
Release archives are written under:
os/yocto/repro-build/dist/
├── dstack-<version>.tar.gz
├── dstack-<version>-uki.tar.gz
└── reproduce.sh
reproduce.sh is emitted when the source tree is clean. The unpacked build
tree and caches remain under os/yocto/repro-build/build-a/.
The bare-metal archive includes the kernel, initramfs, OVMF firmware,
partitioned dm-verity rootfs, platform measurement CBOR files,
sha256sum.txt, digest.txt, and metadata.json. The UKI archive
includes the bootable disk.raw plus its identity and measurement files.
Inspect and verify an archive with:
mkdir -p /tmp/dstack-image
tar -xzf os/yocto/repro-build/dist/dstack-<version>.tar.gz \
-C /tmp/dstack-image
cd /tmp/dstack-image/dstack-<version>
sha256sum -c sha256sum.txt
test "$(sha256sum sha256sum.txt | awk '{print \$1}')" = "$(cat digest.txt)"
Build both production and development flavors
Production is the default. To build both variants once:
cd os/yocto/repro-build
RELEASE_FLAVORS="prod dev" ./repro-build.sh -n
The development archive is named dstack-dev-<version>.tar.gz and records
"is_dev": true in metadata.json.
Check reproducibility
For a release candidate, omit -n:
make os-repro-check
This builds independent build-a and build-b trees and compares the
release-relevant output allowlist. It takes roughly twice the resources of a
single build. Remove both ignored build trees if you specifically need a
from-scratch comparison:
rm -rf os/yocto/repro-build/build-a \
os/yocto/repro-build/build-b \
os/yocto/repro-build/dist
make os-repro-check
Incremental backend development
The reproducible wrapper is the recommended release path. On a host with the
packages listed in os/yocto/repro-build/Dockerfile.repro, the generic
backend entrypoint can also be used directly from the repository root:
./os/build.sh \
--backend yocto \
--flavors prod \
--build-dir "$PWD/os/yocto/bb-build"
This keeps the native BitBake cache in os/yocto/bb-build/ and writes
assembled images under the repository-root images/ directory. Build both
flavors with --flavors "prod dev".
The generic entrypoint dispatches to os/<backend>/build.sh. A future
backend such as mkosi can implement the same artifact-manifest contract without
changing the common assembler or release consumers.
Troubleshooting
A dependency directory is empty
Run:
make os-deps
git submodule status -- os/yocto/deps
Every listed dependency should start with a space, not -.
Docker permission is denied
Ensure docker version works as the same non-root user that owns the
checkout. Do not run only part of the build as root; mixed ownership in
build-a/ makes incremental builds difficult to repair.
A fetch task fails
Yocto fetches many upstream sources. Preserve build-a/, confirm outbound
network and DNS access, then rerun make os-image; completed downloads and
tasks are reused.
docker-compose do_fetch repeatedly shows 0–100%
This is not one archive being downloaded in a loop. Docker Compose has hundreds of independently checksummed Go-module sources, while BitBake's terminal percentage describes only the current source URL. The percentage therefore returns to zero for every module even though the task timer and PID stay the same.
Let the first fetch finish. If it is interrupted, rerun the same command;
completed files have .done markers in the build directory's downloads/
cache and are not downloaded again. To confirm which URL is currently being
fetched during a native make os build, inspect the latest task log:
find os/yocto/bb-build/tmp-mc-* -path '*docker-compose/*/temp/log.do_fetch' \
-print -exec tail -n 5 {} \;
The disk fills up
The largest disposable directories are:
os/yocto/repro-build/build-a/
os/yocto/repro-build/build-b/
os/yocto/bb-build/
They are ignored by Git and can be removed when no build is running. Keep
dist/ separately if you need the release archives.
reproduce.sh is missing
The image archives are still valid. The wrapper intentionally skips generating
reproduce.sh when git status --porcelain reports a dirty source tree,
because that script can reproduce only committed source revisions.