Building your own image
Immos is built by a containerised Bash pipeline on top of Gentoo — every input pinned, every build reproducible. You can run it on any Linux machine with Docker or Podman.
What the build produces
One raw GPT disk image, plus the pieces the update server needs: the compressed system image, the boot image, and a signed checksum manifest. The same artifact boots in a VM, from USB and on an internal disk. Builds are organised in profiles:
| Profile | Produces |
|---|---|
desktop (default) |
The Plasma 6 system image |
console |
No desktop — a fast structural check of the pipeline |
installer |
The live medium carrying the desktop image plus the graphical installer |
Requirements
- Docker or Podman, able to run a privileged container
- Bash, and enough disk for the build cache and output — plan for tens of gigabytes
- Time. The first build compiles the world from source
The pipeline runs inside its own container image, so the host needs nothing else — a Linux machine or Docker Desktop with WSL2 both work.
Running it
$ git clone https://github.com/artlucas/gentoo-immutable-os
$ cd gentoo-immutable-os
$ bash scripts/build.sh # desktop profile → out/immos-<version>.img.zst
$ bash scripts/build.sh --profile console # no desktop; fast structural check
$ bash scripts/build.sh --profile installer # the installer medium
$ bash scripts/build.sh --dry-run # print the stages without running them
$ bash scripts/build.sh --from 40 # resume after a failed stage
bash scripts/enter.sh drops you into a shell inside the builder container, and
bash scripts/run-vm.sh out/immos-<version>.img boots the result in QEMU — including
--test update and --test rollback drills against the image you just built.
Nothing floats: the pins
A build is only reproducible if every input is fixed, so the repository pins all of them:
- The builder itself — a stage3 digest; the container refuses to run on anything else.
- The package tree — a dated snapshot with a checksum, not a moving
latest. - Every package — per-profile lock files in
config/portage/lock/, one exact version per atom. Adding a package to the set and rebuilding stops with a diff until the lock is regenerated. - Every Flatpak —
config/flatpak/apps.lockpins apps and runtimes to exact Flathub commits.
Releases carry a provenance file recording all of it, so a given release can be reconstructed from its inputs without the build host.
Upgrading deliberately: relock
The locks are not “nothing ever moves” — they are “things move when they should, one reviewable
diff at a time”. scripts/relock.sh is that mechanism:
$ scripts/relock.sh --security # GLSA-driven, least-change — the normal patch-release path
$ scripts/relock.sh dev-libs/openssl # release exactly these atoms
$ scripts/relock.sh --all # re-resolve everything against the current tree
A security patch release is then: refresh the tree pin → relock.sh --security → review a
three-line diff → bump the version → build. Everything outside the diff carries the version the
previous release shipped.
Offline, reproducible builds
build.sh --vendor (stage 90) produces a 13–14 GB archive holding everything the pins name:
the tree snapshot, all source distfiles, compiled binary packages, the builder image and the
Flatpak repository. With it:
$ bash scripts/build.sh --offline --vendor-dir out/vendor/immos-<version>
every stage runs with networking disabled entirely — the archive is the whole world. The package list it produces is verified byte-identical to the online build.
Changing what goes in
Everything identity-related — the distribution name, version, update URL, channel, partition
sizes — lives in config/build.conf. The package set is Portage sets under config/portage/,
and the pruning rules that keep the toolchain out of the shipped image are in config/.
Nothing is hardcoded to “immos”: building the pipeline with a different DISTRO_ID produces a
differently branded system with its own update path.
Testing against fixtures
Two throwaway fixtures can stand up alongside a build, for exercising the fleet features end to end:
$ bash scripts/build.sh --with-test-dc # a disposable Samba AD domain controller
$ bash scripts/build.sh --with-test-api # a disposable managed-mode control plane
Signing
Releases are signed with a GPG key whose public half is embedded in the image, so systems built from your fork will only accept updates signed by your key. For development builds you can skip verification; for anything you distribute, do not.
The design documents
The full architecture — immutability model, partition layout, boot flow, update mechanism,
pruning strategy, installer, identity modes and test plan — is written up in the plan/
directory of the repository. It is the reference for why the system is shaped the way it is,
and it is worth reading before making substantial changes.
Contributing
Issues and pull requests go to
the repository. Build problems are more useful with the failing stage’s
log from out/ attached.