# The build subsystem

Building an image is the one step that has to touch the build host. It is isolated from the control plane and fenced two ways.

## No general shell

The build is invoked as an argument array, not a shell string. The runner execs one program — `kixctl-build` — directly, with each argument escaped, so kixctl can run only that one program and only with typed arguments. The program validates every input: the git reference must be commit-pinned, the build attribute is character-restricted, and the image kind is drawn from a fixed set (`container` today; `vm` is the parameterized seam for the appliance). There is no command string to inject into.

## Never as root

The pipeline runs as one unprivileged service user, owns its own Nix cache, and never invokes `nix` as root — the Nix daemon performs the privileged store writes, and `nix build` never needs root. This is enforced by construction rather than vigilance: on the appliance it becomes a systemd unit with a fixed user and cache directory, so ownership is correct from first boot. It sits alongside the control plane's own invariant that it cannot escalate its access.

## The image base

Every built image imports a locked base module that supplies the defaults a custom image must carry — each discovered against a real cluster, not assumed:

- **systemd-networkd is the sole network manager**, so networkd and dhcpcd don't fight over the interface and lose networking.
- **DNS comes from the DHCP lease**, with no hardcoded nameserver, so an image works on any network.
- **git is present** in every container, and the firewall is on with each app opening only its own port.

Two things are set at launch rather than in the image: security nesting, which a NixOS container requires, and the target member, which a cluster placement requires — the deploy path always sends both.

Applications are compiled reproducibly from their own lockfiles — a Node app with `buildNpmPackage` reading the integrity hashes already in `package-lock.json`, a Python app through uv2nix reading `uv.lock`. Neither needs a human to compute a dependency hash, which is what lets kixctl build whatever you push with no one in the loop. What a deployable repository declares is covered in [Your first deploy](../getting-started/first-deploy.md).
