feat(nix): first-class container-runner support in the NixOS module — services.lab.container.* #218
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#218
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What to build
A
services.lab.containersubmodule in the NixOS module, giving container-runner deployments (ADR-0052 / ADR-0053) first-class options instead of hand-wiredextraFlags/extraPackages/ rawsystemd.services.lab.serviceConfigoverrides. Today the module has no container support at all: an operator must assemble the flags,Delegate=yes, podman/passt on the unit PATH, subuid/subgid ranges, and the rootless runtime dir by hand from docs/ops.md.Design grilled 2026-07-23; the decisions below are the contract.
Option surface
Rendered flags:
--container-tools-image claude-code=ref[,codex=ref…](comma-joined) and--container-image <defaultImage>when set, appended to the module's args beforeextraFlags.Host provisioning (all gated on
container.enable)virtualisation.podman.enable = true(pulls in policy.json / registries.conf / crun viavirtualisation.containers), plus podman and passt on the lab unit PATH so preflight's PATH lookup and the pane argv resolve.serviceConfig.Delegate = true— delegate all controllers, matching the ops.md guidance the running deployment was validated against.users.users.<lab user>.subUidRanges/subGidRangesfromsubIdRange— NixOS merges user attrs, so this works for operator-brought users too.RuntimeDirectory = "lab"+RuntimeDirectoryPreserve = "yes"+XDG_RUNTIME_DIR=/run/labin the unit environment. Preserve is load-bearing:KillMode=processkeeps podman-attached containers alive across a lab restart/deploy, and a plain RuntimeDirectory would be wiped underneath them, destroying rootless podman's runtime state (pause-process refs, netns bookkeeping).Eval-time guardrails
container.enable→toolsImages != { }(the seedUser precedent: catch the typo'd deploy at rebuild, not after the service restarted).defaultImage(blank + per-repo refs is a valid deployment).toolsImageskeys against provider IDs (the agentPackages precedent; the server's boot error names the registered IDs, and a nix-side list would drift when new providers land, e.g. #126).Ships with
services.lab.container.*; add a note that rootless image storage lands under$HOME/.local/share/containers= the state dir, so dev images count toward stateDir disk sizing.container.enable = trueand dummy refs, so option typos and assertion regressions fail CI without booting anything.Acceptance criteria
container.enable = truewith toolsImages renders--container-tools-image provider=ref[,provider=ref…](and--container-imagewhendefaultImageis set) into ExecStartvirtualisation.podman.enable, podman+passt on the unit PATH,Delegate=true, subuid/subgid ranges fromsubIdRange, andRuntimeDirectory=labwithRuntimeDirectoryPreserve=yes+XDG_RUNTIME_DIRexportedcontainer.enable = truewith emptytoolsImages;defaultImage = nulldeploys finesubIdRange = nullopts out of user-range provisioning; a custom start/count is honoredcontainer.enable = false(the default) the generated unit is unchanged from today — host-mode deployments see zero diffservices.lab.container.*, including the stateDir image-storage sizing noteHuman Brief
Category: enhancement
Summary: Add a
services.lab.containersubmodule to the NixOS module that provisions everything the container runner's preflight checks — flags, podman/passt, cgroup delegation, subuid/subgid ranges, and the rootless runtime dir — behind one explicitenableswitch.The issue body's "What to build" and acceptance criteria are the authoritative spec (grilled 2026-07-23). Not blocked: the container runner (#205/#214) and per-repo dev images (#207/#217) are merged; this is pure deployment surface.
Why ready-for-human rather than ready-for-agent:
The CI-checkable half (option wiring, flag rendering, the assertion, the closure-build flake check) an agent could ship. But the load-bearing decisions only prove out on a real NixOS host: that
Delegate=trueyields a preflight-green delegated cgroup, thatRuntimeDirectoryPreserve=yesactually keeps live containers working across a lab restart (the KillMode=process invariant this design exists to protect), and that the subuid merge behaves on an operator-brought user. The agent sandbox can run neither podman nornixos-rebuild— same posture as #206: an agent would ship the risky parts blind, and a wrongserviceConfigbit surfaces only as a broken deploy later.Key interfaces (for whoever picks it up):
services.lab.container.{enable, toolsImages, defaultImage, subIdRange}— shapes and semantics per the issue body; all host mutations gated onenable, never on option non-emptiness.extraFlags, so an operator's hand-rolled container flags (today's workaround) still win during migration.virtualisation.podman.enable, the unitpath,serviceConfig.Delegate,serviceConfig.RuntimeDirectory{,Preserve,Mode}, unit envXDG_RUNTIME_DIR,users.users.<user>.sub{Uid,Gid}Ranges.cmd/labalready wires container mode from the flags, and preflight remains the runtime authority on host readiness.Acceptance criteria: the checklist in the issue body.
Out of scope:
Brief addendum — re-labeled
ready-for-agent(maintainer override)The Human Brief above stands as the contract, with one re-scope: this is agent-implementable after all. Unlike #206 (where every acceptance criterion required a live host loop), the CI-checkable half of this change — option wiring, flag rendering, the eval assertion, and the closure-build flake check — is fully verifiable by an agent. The host-behavioral criteria are validated once, by the maintainer, at landing on the dev host, which happens during deploy anyway.
For the implementing agent:
container.enable = trueand dummy refs) is your primary verification vehicle — make it exercise the assertion path too (a closure with emptytoolsImagesmust fail eval).container.enable = falseconfig must be unchanged from today's module output.