Set up & build
Preparing your environment
Before you read: Start here (Day 0) and Linux foundations — the host traps below are explained in terms of user namespaces and cgroup delegation.
Delonix Runtime is Linux-only: every primitive it uses — namespaces, cgroups v2, nftables,
pivot_root, the new mount API — lives in the Linux kernel. You can compile most of the
workspace and run its pure-logic tests on any Linux box with the toolchain below; to run
containers and exercise the live paths you need a host that satisfies the kernel and package
requirements in this page. After it your host passes delonix system info, and when something
fails you can tell a host prerequisite from an engine bug.
A lot of what looks like an engine bug on a fresh machine is a host prerequisite. Read the Known host traps section before you open an issue.
Toolchain#
- Rust toolchain:
1.96.0(pinned inrust-toolchain.toml;rustupinstalls it on the firstcargocall) - Components:
rustfmt,clippy
Install rustup and let it pick up the pinned channel; do not override it
with stable. CI uses exactly the same file (rustup show in every job).
protoc (required to build)#
crates/interfaces/delonix-cri/build.rs compiles the Kubernetes CRI protobuf with
tonic-build/prost, which needs the Protocol Buffers compiler on PATH. The delonix binary
depends on delonix-cri, so a plain cargo build --workspace fails without it:
# Debian / Ubuntu
sudo apt install protobuf-compiler
# Fedora / RHEL family
sudo dnf install protobuf-compiler
Or download a release from https://github.com/protocolbuffers/protobuf/releases. CI installs
protobuf-compiler from apt in every job that compiles.
Optional tools, only for specific gates#
| Tool | Needed for | Where it is pinned |
|---|---|---|
| Python 3.11+ | every scripts/*.py gate (they use tomllib) |
— |
buf v1.73.0 and protoc-gen-openapi v0.7.1 (Go toolchain to install them) |
scripts/ |
contract job in . |
cargo-deny |
the supply-chain check | deny job, config in deny.toml |
Python markdown module |
docs/gen.py (renders ARCHITECTURE.md) |
docs job |
groff |
checking the generated man pages | docs job |
See Clone, build and test for how each one is run.
Kernel requirements#
These are the kernel features the engine relies on. The installer (scripts/install.sh) and
delonix system doctor check most of them for you.
| Requirement | Why | How to check |
|---|---|---|
| cgroup v2 (unified hierarchy) | resource limits and accounting are written to /sys/fs/cgroup |
stat -fc %T / prints cgroup2fs |
| Unprivileged user namespaces | the rootless model: the engine becomes "root" only inside its own user namespace | unshare -r -n true succeeds |
/dev/net/tun |
slirp4netns (rootless networking) and VM taps |
test -e /dev/net/tun |
overlayfs with the new mount API and lowerdir+ (Linux 6.5 or newer) |
container root filesystems are overlay mounts built with fsopen/fsconfig/fsmount, one lowerdir+ call per layer — see ADR-0037 |
uname -r |
br_netfilter loaded, net. |
namespace isolation is enforced in nftables forward chains; without this module traffic between two containers on the same bridge never reaches them, and isolation is silently inert |
delonix system doctor |
KVM (/dev/kvm) |
only for microVMs — see Building microVMs | test -w /dev/kvm |
The 6.5 requirement has no preflight check: an older kernel fails when the first container rootfs is mounted, not at startup (ADR-0037 records this as a deliberate choice).
On older Debian kernels unprivileged user namespaces are switched off by
kernel.unprivileged_userns_clone=0; the installer sets it to 1.
Host packages#
The source of truth is scripts/install.sh, which is also the
official installer (it is published as a release asset). It detects the package manager through
/etc/os-release and supports apt (Debian, Ubuntu and derivatives), dnf (Fedora, RHEL,
CentOS Stream, Rocky, AlmaLinux), zypper (openSUSE, SLES) and pacman (Arch and
derivatives). The installer installs prebuilt binaries for x86_64 and aarch64 (arm64 is
normalised): the asset name is composed from uname -m as <name>-<arch>-linux, and any other
architecture stops with "no prebuilt binary for -v3 variant is an x86-64 microarchitecture level and does not exist there; the pinned
static Cloud Hypervisor, the EDK2 CLOUDHV.fd and hypervisor-fw are x86-64 builds, so they are not
downloaded (a distro-packaged cloud-hypervisor is still installed if the package manager has it) and the VM backend is libvirt; and the QEMU probe asks for qemu-system-aarch64. The
aarch64 release assets exist since v4.2.0 (the v4.1.0 release has none).
Not validated: a complete install on a real aarch64 host, and the QEMU package names per distro
on arm64 — what has been checked is the asset-name composition against the published v4.2.0 assets.
To run containers the engine needs:
| Command | Package (apt / dnf names) | Why |
|---|---|---|
slirp4netns |
slirp4netns |
rootless networking and published ports — without it run -p fails |
newuidmap / newgidmap |
uidmap / shadow-utils |
setuid helpers that map more than one uid into the user namespace; without them images with a non-root user fail in chown() |
nft |
nftables |
SDN firewall, isolation and port DNAT |
ip |
iproute2 / iproute |
veth, bridge and netns plumbing |
conntrack (optional) |
conntrack / conntrack-tools |
cleaning up connections when a port is unpublished |
For VMs, qemu-img, cloud-localds (cloud-image-utils), virsh (libvirt) and/or
Cloud Hypervisor with its firmware — see Building microVMs. For building VM images,
libguestfs-tools (install.sh --with-image-build).
You also need a subordinate uid/gid range for your user in /etc/subuid and /etc/subgid;
without it the user namespace can map only one uid.
The quickest way to a working host#
You do not have to replicate the installer by hand. To install only the host dependencies and configuration, keeping the binary you build yourself:
bash scripts/install.sh --no-binary
Read the flag list at the top of the script first: some flags change host-wide security settings
(--low-ports lets any local program bind ports from 80, --with-image-build makes
/boot/vmlinuz-* world-readable), and --no-tune skips the kernel modules and sysctls, including
br_netfilter. --performance / --no-performance control CPU performance mode, transparent
hugepages with irqbalance, and a user timer that runs system prune --auto --threshold 75; with
neither flag the installer asks about each and, with no terminal, answers no. The CPU part is a
systemd service that stores the boot-time values and puts them back on stop. None of this is
needed for development.
Memory and disk#
There is no fixed minimum. What costs resources is what you run: container images and layers,
VM disks, and the Rust target/ directory itself (debug builds of the whole workspace take
several gigabytes). Keep an eye on free disk space — kubelets in a local cluster start evicting
pods on disk pressure, which then looks like an engine problem.
Diagnosing the host#
Build the binary (see Clone, build and test) and ask it. The commands below are
read-only unless you pass --delegate:
./target/debug/delonix system doctor # is every prerequisite met? says how to fix each
./target/debug/delonix system info # rootless?, cgroup delegation, network infra, counts
./target/debug/delonix system setup # diagnose cgroup delegation
./target/debug/delonix system resources # which controllers are delegated, which flags are ignored
delonix system doctor --strict exits non-zero when a check fails, which is useful in a
provisioning script. If you run these on a machine that already has a Delonix installation in use,
isolate the state first (see Isolating the engine's state).
Known host traps#
Ubuntu 23.10+: AppArmor blocks user namespaces for your dev binary#
Recent Ubuntu sets kernel.apparmor_restrict_unprivileged_userns=1. A binary without an AppArmor
profile then cannot create a user namespace, and the engine dies at unshare() with EPERM —
which reads like an engine bug.
install.sh installs a profile (/etc/apparmor.d/delonix, flags=(unconfined) with userns),
but that profile is bound to one path: <install dir>/delonix (/usr/local/bin/delonix by
default, ~/.local/bin/delonix with --user). A binary you just built at
target/debug/delonix — or copied to /tmp — is not covered.
Options, from least to most invasive:
- Add a second profile for your development path (for example your worktree's
target/debug/delonix), following the same shape as the one the installer writes, and load it withsudo apparmor_parser -r <file>. This touches nothing that is already running. - Install your build to the profiled path (
sudo install -m 0755 target/debug/delonix /usr/local/bin/) — only on a machine where no Delonix workload is in use. The installed binary is what boot units (ExecStart=<exe> container start …) and the servers re-execute, so on a host with live workloads a debug build would silently become the production engine. - Set
kernel.apparmor_restrict_unprivileged_userns=0— this lowers a host-wide boundary; only on a machine you own.
cgroup delegation: some limits are refused, others are not enforced#
Resource limits only reach the kernel if the shell you run the engine from sits in a delegated cgroup. This is a cgroup v2 rule, not a Delonix limitation — rootless Podman has the same requirement. Without delegation the engine does two different things, depending on the flag:
-m/--memory,-c/--cpusand--cpu-weight:container runrefuses before creating anything, with an error that names the fix, and exits 69 (Error::Unavailable, theEX_UNAVAILABLEclass —preflight_resource_limitsinbins/delonix-runtime-bin/src/cmd/container.rs).DELONIX_ALLOW_UNENFORCED_LIMITS=1runs the container anyway, unlimited, with a warning.--cpuset,--io-weightand the--device-read-bps/--device-write-bps/--device-read-iops/--device-write-iopsfamily are checked per controller (preflight_controller_limits, which asksleaf_controllersincrates/adapters/delonix-linux): without thecpuset/iocontroller in the container's cgroup,container runrefuses with exit 69, under the sameDELONIX_ALLOW_UNENFORCED_LIMITS=1escape hatch. Measured on 2026-09-27: before this,--device-write-bps 5mbwrote at 1.6 GB/s and exited 0.
There is no --pids-limit flag; the pids ceiling is a property of the engine's cgroup group, not
of container run.
The common case is an SSH session: its scope sits outside your delegated subtree, and the session cannot move itself in — why, with the commands to see it, is in Linux foundations — Delegation to users. The per-command fix needs no root:
systemd-run --user --scope -p Delegate=yes -- ./target/debug/delonix container run -d -m 128M alpine sleep 60
For long-lived workloads, use a systemd user unit with Delegate=yes. Many hosts delegate only
cpu memory pids to user@.service, and then no scope fixes cpuset/io: a
systemd-run --user --scope -p Delegate=yes only receives what user@.service itself has
(measured: still 1.2 GB/s under --device-write-bps 5mb). The remedy is root-only, once per host —
a drop-in /etc/systemd/system/user@.service.d/50-delonix-delegate.conf with [Service] and
Delegate=cpu cpuset io memory pids, then systemctl daemon-reload and systemctl restart user@<uid>.service
(daemon-reload alone is not enough). delonix system setup prints it, and lists under refused: the flags container run
will refuse on this host. sudo delonix system setup --delegate writes that drop-in whatever the
diagnosis says, runs daemon-reload, and prints the systemctl restart user@<uid>.service still to
do; under sudo it reports on the invoking user's user@<uid>.service (from SUDO_UID), not on root.
A stale delonix on your PATH#
If Delonix is installed on the machine, delonix on your PATH is the installed release, not your
tree. Always run ./target/debug/delonix (or target/release/delonix) when you test a change.
--version shows the commit and the distance from the last tag
(commit: <hash> (+N commits since vX.Y.Z)), because between releases two builds share the same
version number.
Other traps you may hit#
- Ports below 1024 fail rootless with
slirp_add_hostfwd failed: the port is bound byslirp4netns, an unprivileged process. Use a high port, or opt in withinstall.sh --low-ports. - Hosted CI runners (GitHub-hosted) block unprivileged user namespaces. The chaos workflow
detects this and reports
skipped, notsuccess; to exercise the live paths you need a real host, a self-hosted runner or a VM. - VM firmware and image-building traps (the Cloud Hypervisor firmware choice, an old
passtin libguestfs builds,/boot/vmlinuz-*permissions) are covered in Building microVMs.
Next: Clone, build and test — building, installing and testing your tree, and every CI gate as a local command.