Working spec for how malmo ships — from source to a USB stick to a running box. Companion to
SPEC.md,CONTROL_PLANE.md,FIRST_RUN.md,STORAGE.md.
Environment profiles. This doc describes building the
applianceinstall ISO. The samemkosibuilder also emits a lean hosted cloud VM image profile (no Avahi/Samba/NetworkManager/cryptsetup-TPM/mergerfs) paired with a build-tagged slim cloudhost-agent. SeeENVIRONMENT.md# How the profile is realized.
This doc is draft / option-survey. Most sections present alternatives with a recommendation; locked decisions are called out explicitly. The intent is to surface forks before committing.
- The Debian base — release, kernel, what's preinstalled.
- ISO composition — tooling, layout, online vs. offline.
- The installer — what runs between USB-boot and reboot-to-disk.
host-agentpackaging and how it lands on disk.malmo-brainimage build, distribution, first-boot pull.- Versioning and release artifacts.
What it does not cover: update mechanics post-install (separate doc), CI/CD specifics, signing infrastructure (deferred until we have a release to sign).
- Debian 13 "Trixie" (stable). Current as of 2026, fresh enough kernel/userland for modern hardware.
- Tracking testing or unstable would buy newer packages at the cost of stability we cannot afford for a non-technical-user appliance.
Locked: Debian stable. Re-pin when the next stable cuts.
Two real options:
- Stock stable kernel. Whatever ships in Trixie. Conservative, well-tested, but a 2026 stable kernel will already be a year+ behind on hardware support — bad for BYO x86 where the user's NIC / Wi-Fi / GPU may be newer than the kernel knows about.
linux-image-*-bpo(backports kernel). Newer kernel, same Debian packaging discipline. Standard answer for "I want broad hardware support on stable." Used by ProxmoxVE, many appliances.
Recommendation: backports kernel. BYO hardware is a stated pillar (SPEC.md); shipping a kernel that doesn't recognize last year's Wi-Fi chips defeats it. Cost is a slightly larger update surface — acceptable.
The installed GRUB config must set these kernel parameters (GRUB_CMDLINE_LINUX):
psi=1— enables the Pressure Stall Information accounting (/proc/pressure/*) that theram-pressurehealth detector (HEALTH.md# Detector catalog) reads. Debian builds the kernel withCONFIG_PSI=ybutCONFIG_PSI_DEFAULT_DISABLED=y, so PSI returns no useful data at runtime unlesspsi=1is on the cmdline. Without it the detector silently reads zeros and never fires — a false all-clear. Cost is negligible (a few per-cgroup counters).
- Include
firmware-linux,firmware-iwlwifi,firmware-realtek,firmware-amd-graphics,firmware-misc-nonfreeand similar. Non-free firmware is now in Debian's official installer by default (since Bookworm); we follow suit. Without this, half of laptops won't have working Wi-Fi at first boot.
Minimum to be a malmo box:
systemd,systemd-cryptenroll,cryptsetup— boot, encryption, TPM auto-unlock (STORAGE.md).docker-ce(ordocker.iofrom Debian; see below) — runtime for everything.avahi-daemon— mDNS publishing for*.localapp hostnames and SMB service discovery (_smb._tcp).caddy— only if we ship it on host; if it runs as a container under the brain (perCONTROL_PLANE.md), skip on host.malmo-host-agent— our own.deb.openssh-server— SSH daemon, scoped to LAN + mesh via nftables (see "SSH" below).samba— SMB file shares for cross-device access (STORAGE.md# Cross-device access).mergerfs— userspace union for data drives (STORAGE.md# Data drives). Activates whenever a data drive is present.nftables— firewall, scoping SSH and SMB to LAN + mesh.- Standard base utilities (
curl,ca-certificates,tpm2-tools,lvm2,e2fsprogs,cryptsetup-initramfs).
Open: docker-ce (upstream Docker repo) vs. docker.io (Debian-packaged). Upstream is fresher and what the Docker docs assume; Debian's package lags but integrates more cleanly with apt security updates. Lean toward docker-ce from Docker's own apt repo — most of our app authors test against upstream Docker.
openssh-server is installed and enabled at boot — sshd listens on :22 from first boot. However, no account can authenticate by default: sshd_config.d/malmo-allowed.conf carries an empty AllowUsers directive, so sshd rejects every account regardless of whether the password is valid. Per-account opt-in (Settings → My account → Enable SSH) adds the user to AllowUsers and reloads sshd. The user's malmo password — the same one they use for the dashboard — is what authenticates them; SSH does not have its own password (AUTH.md # Device access).
Why daemon-on-but-no-account instead of daemon-off-until-toggle:
- The "turn on SSH" UX is a single toggle in Settings, with no host-level service restart visible to the user. The brain calls host-agent to edit the allowlist; the daemon was already running.
- An attacker on the LAN sees an open :22, but no account is in the allowlist — sshd rejects connections at auth-name resolution, before evaluating credentials. Blast radius is bounded by
AUTH.md's opt-in mechanics. PermitRootLogin no,PasswordAuthentication yes(sshd accepts the user's malmo password; a public key can also be added to~/.ssh/authorized_keysfor key-based login).
Network scope: LAN + mesh only, structurally. An nftables rule on :22 default-denies and allows only:
- RFC1918 source ranges:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16(the LAN). - The mesh interface (
tailscale0/headscale0) when present — devices the user has paired viaMALMO_NETWORK.md.
SSH from the public internet is structurally blocked, not relying on per-account opt-in alone. A port scan from outside sees a closed port, not a refused-auth banner. The path to "SSH to my box from outside" is "pair the device on the mesh" — same trust model the user already learns for the dashboard. Interface-agnostic by design (nftables on source IP, not ListenAddress on a NIC name), so changing NICs / adding Wi-Fi doesn't break it.
Implemented as a drop-in at /etc/nftables.d/malmo-ssh.conf, owned by the .deb. Mesh interface name is templated at host-agent startup based on which mesh client is installed.
- Desktop environment, X/Wayland session manager (except as needed for the installer — see #3).
- Anything from
tasksel's "standard" set beyond what we explicitly list.
Decided (2026-06-16): Option C — mkosi. It is malmo's single image builder, for the install ISO, the cloud VM image, and the QEMU test image alike. See "Decision" below; DECISIONS.md 2026-06-16 carries the delta. The four options below are kept as the record that produced the call.
The fork, as it stood — four real options:
Debian's official meta-tool for building live + installer ISOs. Used by Kali, Tails, official Debian Live.
- Pros: Designed exactly for this. Handles squashfs, bootloader (GRUB/syslinux for BIOS+UEFI), hybrid ISO/USB, package selection, hooks for customization. Mature, well-documented, Debian-blessed.
- Cons: Configuration is a sprawl of directories and shell hooks. Debugging is "read the source." The tool is in maintenance mode — works fine, but few new features.
The installer Debian ships on its own ISOs. Drive it via a preseed file (preseed.cfg).
- Pros: The most boring, well-trodden path. Massive deployments use it.
- Cons: Preseed is a key-value config file with awkward escape rules. Customizing the installer's appearance (malmo branding, custom screens) means patching
cdebconfthemes and is genuinely painful. Conditional logic (e.g., "if no second disk, skip step 2") is a pre-script hack. - Verdict: Right for a sysadmin tool, wrong for a consumer appliance.
systemd-team's modern image builder. Declarative TOML config, can produce disk images, ISOs, container images. Used by Fedora CoreOS-adjacent work and increasingly in the systemd ecosystem.
- Pros: Clean config. First-class support for the kind of immutable / A/B image we expect to migrate to (
SPEC.md"OS update model"). Aligned with systemd, which we depend on heavily. - Cons: Newer; less battle-tested for Debian specifically (better support for Fedora/Arch). Smaller community for "I'm building a Debian appliance" recipes.
- Strategic angle: if we're going to A/B immutable later anyway, picking mkosi now means one tool for both v1 and the future. live-build has no story for A/B images.
Roll our own. What Ubuntu's modern installers do under the hood.
- Pros: Full control. No tool quirks to work around.
- Cons: We become the maintainers of an ISO builder. Many person-weeks to match what live-build gives for free.
- Verdict: Reject. Premature DIY for a problem with mature solutions.
Locked: mkosi is the single image builder — for the install ISO, the cloud VM image, and the QEMU test image. This overturns the earlier "live-build-for-v1, migrate-to-mkosi-later" recommendation (DECISIONS.md 2026-06-16). One builder, one config, one artifact definition for every target.
Why mkosi-now rather than live-build-then-migrate:
- The test lane is already mkosi, all the way up the stack.
dev/test-qemu/builds the full control plane (host-agent → brain → Caddy + UI), boots it undermkosi qemu, andmkosi-repartalready produces a LUKS2+ext4 root that TPM-unseals and switch-roots (TESTING.md# Full-stack control-plane integration;docs/progress/luks-tpm-enrollment.md). Shipping live-build for the ISO would mean maintaining a second builder that must stay byte-identical with the test image to hold the "live fs == installed fs" invariant (# 3). mkosi makes that invariant trivially true — there is one artifact. - Systemd-native is the right substrate for malmo. We lean hard on systemd —
systemd-cryptenroll+ TPM unseal, UKI,systemd-boot,cryptsetup-initramfs. mkosi is the systemd team's own image tool, so partitioning / LUKS / TPM / UKI-signing are first-class rather than bolted on. (Umbrel, on the same Debian base, assembles a Docker-built rootfs + Rugix + Mender to get the equivalent; mkosi collapses that into one pipeline.) - One config emits every target. The same mkosi definition produces the flashable install ISO and a cloud VM image (qcow2 / raw) for the hosted-in-cloud product. live-build has no cloud-image story; that would be a third path.
- A/B-immutable is the stated future (
SPEC.mdOS update model). live-build has no A/B story; mkosi's disk-image output A/B-swaps natively. We are not shipping A/B in v1 — v1 is mutable Debian + a flash-an-ISO install — but picking mkosi now means that future lands with no re-tooling.
What this decision does not settle (kept open — see NEXT.md):
- The OTA update orchestrator. mkosi's presumptive partner is
systemd-sysupdate, but that is not chosen here. Umbrel uses Mender, Home Assistant OS uses RAUC — both with a deeper production track record thansystemd-sysupdatefor Debian appliances. Naming the orchestrator waits for the A/B work. - The interactive installer is unchanged. mkosi vs live-build is only how the bootable artifact is assembled — malmo still ships the guided first-run installer of # 3 /
FIRST_RUN.mdPhase 1 (disk selection, recovery passphrase, confirm-wipe). The USB stick boots that installer, which writes the OS to the machine's internal disk. We are not adopting the competitors' direct-flash-the-image-onto-the-target model.
Knowingly accepted costs:
- mkosi's Debian support is thinner than live-build's (it is better-trodden for Fedora / Arch). The LUKS/TPM bring-up already paid down the riskiest part of that on a real Debian boot, but expect occasional sharp edges a live-build user would not hit.
- A live installer ISO that boots a session is live-build's home turf (the Tails / Kali pattern), and is the one part of mkosi's fit not yet proven in-repo — the test lane boots a disk image, not a live-session ISO carrying the kiosk installer (# 3). Validating mkosi's live-ISO output is a follow-up, not a reason to keep a second builder.
- ⚠ Resolved 2026-06-17 (#199): mkosi emits no ISO — and malmo no longer wants one. Investigating this exact follow-up found mkosi 26's output formats are
{confext,cpio,directory,disk,esp,none,portable,sysext,tar,uki,oci,addon}— there is noiso/ISO9660 format (and noxorriso/El-Torito code in the package). mkosi builds GPT disk images. The call (maintainer, 2026-06-17): drop the literal.isoentirely; the bootable artifacts are disk images — aqcow2/rawfor the cloud VM and arawdd'd to a USB stick for bare metal. Optical-media / CD-DVD boot is explicitly out of scope. The "live fs == installed fs" invariant (# 3) is unaffected — aFormat=diskroot is what gets booted/laid down — and "mkosi is the single builder" holds exactly (this is mkosi's native distribution model). The cloud VM image is the priority target; bare-metal USB follows (#196epic ordering). SeeDECISIONS.md2026-06-17 andprogress/iso-mkosi-finding.md.
- ⚠ Resolved 2026-06-17 (#199): mkosi emits no ISO — and malmo no longer wants one. Investigating this exact follow-up found mkosi 26's output formats are
FIRST_RUN.md Phase 1 specifies the installer's user-visible flow: hardware check → disk selection → recovery passphrase → confirm wipe → install → reboot. This section is how that flow runs.
- Model 1 — Custom TUI (text-mode, ncurses-style). Lightweight, ugly, fine for tinkerers, wrong for the long-term audience.
- Model 2 — Custom GUI in a minimal Xorg/Wayland session. Boot a minimal desktop, run a malmo-branded GTK/Qt app. Pretty, heavy on ISO size and dev work.
- Model 3 — Web installer in a kiosk browser. Boot a minimal compositor (
cageorweston --kiosk), launch Chromium pointed at a local installer service (Go binary serving HTTP onlocalhost). The installer service does the actual work (partitioning, LUKS, TPM enrollment, file copy).
- Reuses our web stack — same TypeScript framework, same components, same designers as the post-install dashboard. Visual consistency from USB-boot to dashboard.
- The installer service is a sibling to
malmo-brainin shape: Go binary, HTTP API, but its job ends at first reboot. We can borrow patterns and even some packages. - ISO cost: ~150–250 MB for compositor + Chromium. Acceptable on a multi-GB ISO.
- The same UI language carries forward — no jarring "install looks like a 90s setup, then suddenly it's a polished web app."
ZimaOS and a couple of other appliance OSes use this exact pattern. It's well-trodden.
- Probe hardware (CPU, RAM, disks, UEFI, TPM2). Refuse with a clear message if any hard requirement (
FIRST_RUN.md) fails. - Present disk picker + recovery-passphrase screen.
- On confirm:
a. Partition target disk(s) (GPT, ESP + LUKS-encrypted root).
b.
cryptsetup luksFormat, generate recovery passphrase, enroll TPM2 withsystemd-cryptenroll. c. Lay down the OS image (squashfs → ext4 copy, or rsync from the live filesystem). The installer's own live environment is essentially the same image we lay on disk. d. Install GRUB to the ESP, configure for UEFI. e. Runupdate-initramfsso initramfs has TPM-unlock support. - Show recovery passphrase, require user confirmation.
- Reboot.
The same root filesystem the live ISO boots from is what gets copied to disk. No separate "live image" vs. "installable image." Means everything we test in the live environment is what runs post-install. With one mkosi-built artifact (# 2) this invariant is structural rather than a discipline to maintain across two builders.
Three options:
- A — Ship as
.debin our own apt repo. ISO build pulls it during package selection. Updates ride apt. Standard Debian. - B — Bake the binary directly into the live filesystem at ISO build time (no
.deb, just a file + a systemd unit). Simpler, but no apt-managed update path. - C — Distribute as a container alongside the brain. Inverts the architecture — host-agent is the one thing that should be on the host, not in a container (
CONTROL_PLANE.md). Reject.
Recommendation: A. Ship malmo-host-agent.deb from our apt repo.
- Native package, native systemd unit, native logs.
- apt is how host-agent updates until we move to A/B images. When we do, the
.debgets baked into the immutable image and the apt path retires. Cheap migration. - Our apt repo (
apt.malmo.networkor similar) hosts this one package for v1. Adding more later is mechanical.
The repo is signed; the ISO build trusts our key. Key management is a release-infra concern, deferred to the release-infra doc.
Per CONTROL_PLANE.md: brain runs as a container, supervised by host-agent.
- Multi-stage Dockerfile. Build stage compiles the Go binary (static, CGO disabled where possible). Runtime stage:
debian:trixie-slimwith thedockerCLI + Compose plugin bundled (docker-ce-cli+docker-compose-pluginfrom Docker's official apt repo — the same trusted source as the host engine, per the Docker-package-source decision below). Not distroless: the brain orchestrates apps by shelling out to thedocker/docker composeCLI (internal/lifecycle/docker.go), which a distroless runtime — no shell, no binaries — cannot host. Multi-stage already keeps the Go toolchain out of the final image; the bundled CLI is a runtime dependency it can't trim, putting the image at ~256 MB (measured in M0, #163) — immaterial against the multi-GB app images the box pulls, and slim stays debuggable (it has a shell). SeeDECISIONS.md2026-06-13 for the flip off distroless. - Output is a single OCI image, tagged
vX.Y.Zandlatest(latest only on stable channel).
- A — Public registry (
ghcr.io/malmo/brainor Docker Hub). Pull at first boot. Simple, no infra to run beyond a registry account. Requires internet at first boot. - B — Self-hosted registry (
registry.malmo.network). Same as A but we own the namespace and don't depend on GitHub/Docker policies. Modest VPS cost. - C — Bundle the image in the ISO. Image is loaded into Docker at install time via
docker load. Works offline at first boot. ISO grows by the image size (~256 MB for the slim-with-CLI brain image, measured in M0 #163 — see the Build section above — still small against the multi-GB app images the box pulls).
- Bundle a pinned brain image in the ISO so the box boots and is functional with zero internet.
- Self-hosted registry for ongoing updates. host-agent (or the brain itself) pulls newer tags from
registry.malmo.networkwhen online. - Self-hosted over public-registry-only because: (1) a
malmonamespace on Docker Hub is not guaranteed; (2) we already needmalmo.networkinfra for the mesh, adding a registry is incremental; (3) avoids dependency on a third party's pull-rate-limit policy. - We can mirror to a public registry as a redundancy story, but it's not the source of truth.
- host-agent starts (systemd, after Docker).
- host-agent checks
/var/lib/malmo/brain-image.tar(bundled in ISO) — if Docker doesn't already have the image,docker loadit. - host-agent pulls the latest tag from
registry.malmo.networkif online and a newer version exists. (Behavior on offline: keep the bundled version. Behavior on update failure: keep current. Never break boot.) - host-agent starts the brain container with the configured pin.
- Brain takes over from there — Caddy,
malmo-ui, sidecars, etc. (CONTROL_PLANE.md).
The dashboard ships as a second OCI image, built and distributed the same way as the brain. WEB_UI.md owns the stack and deploy model; this section covers only how the image is built and lands on a box.
- Base
caddy:alpine, with the built UI bundle (web-ui/dist) baked in at/srv/uiand the trivial SPA Caddyfile (serve/srv/ui, fallback toindex.html, gzip/brotli/ETag on by default). No build-stage Go compile — the bundle is produced by the UI's ownvite buildupstream of the image build (WEB_UI.md). - Output is a single OCI image, tagged
vX.Y.Zandlatest(latest only on stable channel) — the samevX.Y.Zas the brain, one repo version (# Versioning, above;WEB_UI.md# deploy + update flow).
Same as the brain (# 5 Distribution): bundled in the ISO for offline first-boot (docker load from a pinned tarball) and pulled from registry.malmo.network for ongoing updates. Both images appear together in the release manifest (RELEASE_MANIFEST.md); the updater recreates only what changed (WEB_UI.md # deploy + update flow).
malmo-ui is not started by host-agent. The brain launches it as part of the control-plane stack, alongside Caddy (CONTROL_PLANE.md # Locked: the dashboard UI is a brain-launched container). host-agent's brain bootstrap (# First-boot brain bootstrap) ends at the brain; the brain brings up everything downstream.
All artifacts of a release share the one vX.Y.Z from the repo VERSION file (# Versioning, above) — there is no independent per-component tag to keep in sync.
malmo-vX.Y.Z-amd64.qcow2— the cloud VM image (priority target; the hosted product provisions tenants from it —ENVIRONMENT.md# Provisioning). Emitted by mkosiFormat=disk.malmo-vX.Y.Z-amd64.raw— the bare-metal install medium,dd'd / flashed to a USB stick (the "old laptop in the pantry" path). Same mkosiFormat=diskrootfs; not optical media (no.iso— see # 2's 2026-06-17 resolution andDECISIONS.md).malmo-host-agent_X.Y.Z_amd64.deb— published toapt.malmo.network.registry.malmo.network/malmo/brain:vX.Y.Z— the brain image.latesttag advances on stable channel.registry.malmo.network/malmo/ui:vX.Y.Z— the dashboard image. SamevX.Y.Zas the brain (one repo version); both bundled in the ISO for offline first-boot.- As-built:
CI / Cloud image(.github/workflows/ci-cloud-image.yml) additionally attachesmalmo-vX.Y.Z-amd64.raw.xz+malmo-vX.Y.Z-amd64.raw.xz.sha256to the tagged GitHub Release, on the same publish gate as the Hetzner snapshot upload above..raw.xznames the actual shipped format — this lane's mkosi build produces.rawdirectly (xz-compressed for the upload), not a qcow2 conversion — so a release carries its own reproducible, provider-neutral image asset fetchable from the Release page alone, alongside the Hetzner snapshot path.
- Stable — what
malmo.com/downloadpoints at. Default for all installs. - Beta — opt-in via Settings. Same artifacts, different repo / tag suffix.
- (No nightly in v1. Internal CI builds exist but aren't a user-facing channel.)
A box's channel determines which apt repo it follows for host-agent and which brain tag it tracks.
One repo version for the whole monorepo (vMAJOR.MINOR.PATCH), not independent per-component SemVer (DECISIONS.md 2026-07-16, flipping the two bullets this section used to carry). host-agent, malmo-brain, and malmo-ui all ship from one commit in one repo — an independent counter per component was bookkeeping with no consumer once that was true.
VERSION— a plain-text file at the repo root, the single source of truth. It holds the last released version and changes only in the dev->main release PR (docs/dev/contributing.md# Release model): bumpVERSION-> merge dev->main -> a push tomainauto-tagsvX.Y.ZmatchingVERSION(.github/workflows/release.yml, no-op if the tag already exists) -> the tag + GitHub Release are created automatically and the image build+publish is triggered directly (not via the tag-push event — see the workflow's header comment for why). No-devsuffix, no "next target" bookkeeping between releases.- Every build stamps two fields, not one: the repo version (from
VERSION) and the git commit it was built from (git rev-parse --short HEAD), via-ldflags -Xintointernal/version— e.g.malmo-brain --versionprintsmalmo 0.4.0 (g1a2b3c). On a tagged release the commit is the tag's commit; on a dev build between releases it isn't, and that's visible without needing a suffix on the version string itself.VERSION(notgit describe) is the source CI asserts a pushed tag against, because the brain's container build and the mkosi cloud-image build both run from contexts without full.githistory (the Dockerfile's build context excludes.gitentirely —.dockerignore). - The image inherits the repo SemVer, not CalVer. The ISO/cloud-image build used to be planned as
YYYY.MMon the reasoning that it's a snapshot of host-agent + brain + Debian + apps, not a single component — that reasoning assumed independent per-component versions needed reconciling into something else for the image. With one repo version, brain/UI/host-agent/image are all just "the same commit," so the image takes the samevX.Y.Zthe commit already has. One commit, one identity, not two. - The image still carries a manifest listing the exact versions of every component it bundles (Debian base version, kernel, etc. — components genuinely external to this repo).
Not locking specifics, but the rough shape:
Source (host-agent, brain, UI)
│
▼
CI (build, test)
│
├──► host-agent .deb ──► apt.malmo.network
├──► brain image ─────► registry.malmo.network
└──► ui image ────────► registry.malmo.network (caddy:alpine + bundle, see WEB_UI.md)
│
▼
mkosi image assembly (Format=disk)
│
▼
malmo-vX.Y.Z-amd64.qcow2 (cloud VM, priority)
malmo-vX.Y.Z-amd64.raw (bare-metal USB)
│
▼
releases.malmo.network
│
▼
stable.json (+ minisig) — see RELEASE_MANIFEST.md
GitHub Actions or self-hosted CI — TBD, not architecturally interesting at this stage.
- Base: Debian stable (currently Trixie / 13).
- Kernel: Debian backports kernel for hardware support on BYO x86.
- Non-free firmware bundled for Wi-Fi and GPU support out of the box.
- Image tooling:
mkosi(decided 2026-06-16,DECISIONS.md). One builder for the cloud VM image, the bare-metal USB install image, and the QEMU test lane; systemd-native, and A/B-ready for the immutable future. Overturns the earlier live-build-for-v1 recommendation. (⚠ #199, 2026-06-17 resolved: mkosi has no ISO9660 output — it builds GPT disk images, and malmo no longer ships a literal.iso. Artifacts are aqcow2/rawcloud image (priority) and arawUSB image; CD/DVD/optical boot is out of scope. See # 2's resolution +DECISIONS.md2026-06-17.) - Installer execution model: kiosk web installer. Minimal compositor (
cage/weston --kiosk) + Chromium pointed at a local installer service. Closest production reference: Fedora's Anaconda Web UI. - Docker package source:
docker-cefrom Docker's official apt repo. Revisit if Docker Inc. policy changes; swap todocker.iois a one-line apt source change. host-agentships as a Debian package from our own apt repo, not as a container.malmo-brainships as an OCI image,debian:trixie-slimruntime with thedockerCLI + Compose plugin bundled (the brain shells out to them; distroless can't host them —DECISIONS.md2026-06-13), from our own registry, also bundled in the ISO for offline first-boot.malmo-uiships as a second OCI image (caddy:alpine+ baked UI bundle), from our own registry, also bundled in the ISO. Launched by the brain, not host-agent (CONTROL_PLANE.md).- Same root filesystem serves both the live (installer) environment and the installed system.
- SSH daemon enabled at boot; no account can authenticate until per-user opt-in (
AUTH.md# SSH access). Root login disabled. - Channels: stable only in v1, no beta, no nightly. Beta is additive when triggered (see
RELEASE_MANIFEST.md). - Versioning: one repo SemVer for the whole monorepo, the image inherits it.
VERSIONat the repo root is the source of truth; every build additionally stamps the git commit as a separate field. No independent per-component counters, no CalVer for the image (DECISIONS.md 2026-07-16, flipping both prior positions).
Tracked centrally in NEXT.md. Resolutions land back here (or in DECISIONS.md if they flip a position).