- Status: done
- Date: 2026-07-08
- Specs touched:
ENVIRONMENT.md(# Storage)
The hosted cloud image bakes a fixed 8 GiB root so the shipped raw stays small (sparse), but nothing grew it onto the (much larger) provider disk, so a provisioned box ran on ~8 GiB. Docker image storage and the brain's SQLite store share that one volume, so a single app install can fill it; once full the brain's first store write on the SSO landing fails and login returns a bare HTTP 500 — the box looks offline though it is up, and self-heals only if the failing install rolls back and frees space. This closes that gap by growing the root to fill the whole disk on every boot.
- Runtime repart definition —
dev/cloud/mkosi.extra/usr/lib/repart.d/50-malmo-grow-root.conf(Type=root,GrowFileSystem=yes, no size cap). systemd-repart matches it to the single existing root partition and, with noSizeMaxBytes, extends it to consume all free space;GrowFileSystemonline-grows the ext4 (root is mounted rw). NoFormat=, so the populated ext4 is never treated as empty. This is deliberately separate from the build-time layout indev/cloud/mkosi.repart/(which pins the root at exactly 8 GiB so the shipped raw stays small): build-time pins, runtime uncaps. malmo-grow-root.service— aType=oneshot/RemainAfterExit=yesunit runningsystemd-repart --dry-run=no,ConditionVirtualization=!container,SuccessExitStatus=76 77(76 = no root block device, 77 = no GPT table — the "nothing to grow" cases). OrderedBefore=docker.service host-agent.serviceso the disk is full before anything writes to the shared volume. Enabled by a.wantssymlink inmkosi.postinst.chroot(same pattern asmalmo-metadata-firewall).- Fail-closed gating —
Before=only orders starts; it does not stop the write-heavy services from starting if the grow fails. So two hosted-only drop-ins under the trackedmkosi.extra/addRequires=malmo-grow-root.service:docker.service.d/10-malmo-grow-root.confandhost-agent.service.d/10-malmo-grow-root.conf. If the grow ever fails, docker and the brain refuse to start on an un-grown 8 GiB root rather than run into the disk-full 500. The gate is a drop-in (not an edit to the unit) on purpose:host-agent.serviceis shared with the appliance (dist/systemd/host-agent.service, also used bydev/test-qemuand the nspawn boot-chain), and the appliance has nomalmo-grow-root.service, so theRequires=must be scoped to the hosted profile. - Lean package set — on trixie the
systemd-repartbinary is split out of thesystemdpackage, so the lean image depends onsystemd-repartexplicitly; it pulls onlylibfdisk1on top of what systemd already brings (nolibcryptsetup, so the cryptsetup cut is preserved). Both are added toexpected-packages.txt; the lean guard confirms the delta. - Boot-proof assertion — a new check in
cloud-assertions.shassertssystemd-repartis present in the lean image and thatmalmo-grow-root.servicereachedactive(held byRemainAfterExit). The QEMU boot-proof disk is fixed-size with no spare space, so the grow itself is a harmless no-op there; the assertion proves the tool is present and the unit is wired, not that a real disk grew. - Spec —
ENVIRONMENT.md# Storage documents the boot-time grow and that hosted v1 is deliberately one volume for everything (system, docker images, app data, store); splitting app/data onto its own volume is the external-drives follow-up (# Open questions), not this profile.
- Real full-disk growth is not exercised in CI. The fixed-size QEMU boot-proof lane only exercises the no-op path (present + wired + active). Partition + ext4 actually extending onto a larger disk is the acceptance step on a real provider VM, not this lane.
- Double execution, by design (for now). The
systemd-repartpackage ships its ownsystemd-repart.service, statically enabled, which already runs the same/usr/lib/repart.d/*.confat boot — so the grow runs twice. This is harmless (systemd-repart is idempotent; a second pass on an already-grown disk is a no-op). The custommalmo-grow-root.serviceexists only to add the docker/host-agent ordering and the fail-closedRequires=gate, which the stock unit cannot express. Consolidating onto a stock-unit drop-in (contributing only the ordering/gating, dropping the duplicateExecStart) is a deliberate deferral, pending a live re-test of the growth path — the current mechanism is the one verified on a real provider box, so it is kept intact. - Pre-existing, unrelated boot-proof failure.
make test-cloud-qemucurrently fails at the unseeded/setupcheck (getsHTTP 503via Caddy, expects 403). This reproduces on a clean build ofmainwith this change stashed out, so it is pre-existing and independent of this change: the box's Caddy isn't serving the/apiroute to the brain at probe time (the brain unconditionally returns 403 for hosted/setup,internal/api/auth.go). Likely fallout from the recent hosted SSO / wildcard-TLS work; tracked separately, not introduced here.
- Consolidate the grow onto a single unit (drop-in on the stock
systemd-repart.servicecontributing only the ordering + gating) once the growth path can be re-tested live, removing the double execution. - Exercise real full-disk growth in an automated lane (a provider-shaped disk with spare space) so partition + ext4 extension is covered without a manual on-box acceptance step.