# NixOS module: `services.nxdns` Owner ruling (2026-10-20): ship `nixosModules.default` from the flake, with the options `enable`, `package`, `configFile` and `openFirewall`, file mode only, no `settings` attrset, and the shipped `nxdns.service` reused through `systemd.packages` plus a drop-in. The owner left Nix in CI, the VM test and a distinct exit code for `SchemaTooNew` to the implementer, within one constraint: the self-hosted Gitea runner (`../mial.net/manifests/gitea/`) is shared and must not be monopolized. ## Why The flake already shipped a package and had one consumer, `rpi.mial.net`, which wrote its own module. Four parts of that module are not specific to the Pi, and two encode incidents: the build-time `nxdns import` validation, and `restartTriggers`, without which a configuration-only deploy restarted nothing (2026-08-16). A package with no service is half an install on NixOS. ## What ships - `nix/module.nix`. `configFile` goes through `nxdns import --data-dir $TMPDIR/validate` from `package` in a `runCommand`, and the result is `/etc/nxdns/config.zon`. The `nxdns` system user and group. `systemd.packages = [ package ]`, and a drop-in that clears `ExecStart` and sets `run --config=/etc/nxdns/config.zon`, clears `ConfigurationDirectory` because `environment.etc` owns `/etc/nxdns`, adds `wantedBy` and `restartTriggers`. `openFirewall` opens 53/udp and 53/tcp and nothing else. - The package installs the tarball's `nxdns.service` to `lib/systemd/system/` with `ExecStart` pointed at its own binary. `--replace-fail` makes a changed `ExecStart` line in the unit a package build failure, not a silent mismatch. - Not shipped: database mode, a Nix-to-ZON renderer, and the Pi's choices about its own resolver, which stay in `rpi.mial.net`. ## Decisions left to the implementer - **No Nix in CI.** No job installs Nix today. The checks need a nixpkgs closure and, for the VM test, KVM; downloading that on every push costs the shared runner disk and network, and the runner's cache already caused a disk-pressure eviction on 2026-08-23. The cut's existing `nix flake check --no-build` evaluates both checks at every release, which catches a module that no longer evaluates. - **The VM test runs on the developer's host.** `checks.x86_64-linux.nixos-module` and `checks.x86_64-linux.nixos-module-rejects-config` are built locally, with KVM, after any change to the module, the checks, the package or the unit. AGENTS.md says so. Whether the runner node exposes `/dev/kvm` was not checked, and nothing here depends on it. - **No new exit code.** `SchemaTooNew` is already in `config/faults.zig`, so `run` exits 2 for it and the unit's `RestartPreventExitStatus=2 64` stops instead of restart-looping (`src/app.zig` tests the mapping). A NixOS rollback onto a migrated database therefore stops the unit, and `docs/how-to/upgrade.md` describes the recovery. The exit-1 behaviour in the Pi module's notes dates from 0.0.2. ## Acceptance - `nix flake check --no-build` passes. - `nixos-module`: a VM with the module and `openFirewall` answers `nas.lan` from a local record; `systemctl cat` shows the packaged unit and the drop-in; the service runs as `nxdns`; the effective `ConfigurationDirectory` is empty, `ReadOnlyPaths` still holds `/etc/nxdns`, and `/etc/nxdns` belongs to root; port 53 is open over UDP and TCP; `switch-to-configuration test` into a specialisation that changes only `configFile` gives a new `InvocationID` and the new answer. Without `restartTriggers` the test fails on the restart assertion, and without the `ConfigurationDirectory` clear it fails on that one (each checked once by removing the line). - `nixos-module-rejects-config`: a system build with an invalid regex rule fails, and its log carries `FAIL rules[0].pattern`. - `docs/how-to/install-with-nix.md` documents the module, the options, the store-readability of `configFile`, and rollback. ## After the release `rpi.mial.net` replaces its module with `services.nxdns` and keeps its Pi-only parts, through the rpi-nixos-iac peer.