Files
nxdns/specs/nixos-module.md
T

4.0 KiB

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.