8.8 KiB
Install nxdns with Nix
Adds nxdns to a NixOS machine as a flake input pinned to a release tag and runs it with the flake's services.nxdns module. At the end the machine serves DNS on port 53 from a configuration file that the pinned binary checked at build time, and a Renovate custom manager opens a pull request when a new tag appears.
nxdns publishes its own flake.nix. Its packages do not build nxdns from source: each one fetches the release tarball for the target and pins its SHA-256 hash, so a changed byte fails the build. The two supported systems are aarch64-linux and x86_64-linux, both static musl builds that need nothing on the host.
For the signature and checksum checks a human does once, see verify a release. For what each configuration field means, see the configuration reference.
Verification: the commands in step 1 and step 2 were run on the machine that wrote this page, against the flake at nxdns 0.0.16.
nix flake check --no-buildpassed andnix buildproduced a binary that printednxdns 0.0.16. Step 3 was verified on 2026-10-20 by the flake's own checks, built with Nix 2.35.3 on an x86_64 host with KVM and the module running the 0.0.22 release:checks.x86_64-linux.nixos-moduleboots a NixOS VM with the module andopenFirewall, getsnas.lanfromdig @127.0.0.1, finds the packaged unit with its drop-in insystemctl cat, finds port 53 open over UDP and TCP, and switches to a configuration-only change that restarts the service and changes the answer;checks.x86_64-linux.nixos-module-rejects-configfails a system build on a bad regex withFAIL rules[0].pattern: '(' is not a valid regex pattern. The Renovate snippet in step 4 is copied from the shaperpi.mial.netalready uses for another flake input of the same author.
1. Add the input, pinned to a tag
In the consuming flake:
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nxdns = {
url = "git+https://git.mial.net/mokhtar/nxdns.git?ref=refs/tags/v0.0.16";
inputs.nixpkgs.follows = "nixpkgs";
};
};
Pin a tag, not a branch. A branch pin moves the version under you at the next nix flake update, and the hashes in the flake belong to whatever release that branch last cut.
The follows line is not cosmetic. Without it Nix fetches and evaluates nxdns's own nixpkgs as a second nixpkgs, which costs a download and an evaluation for a package that only needs stdenv, fetchurl, and lib.
Write the lock entry:
nix flake lock
2. Check what you pinned
nix flake check --no-build
nix eval .#packages.x86_64-linux.default.outPath
Evaluation does not fetch the tarball. The download happens at build time, and the hash in nxdns's flake is what the build asserts the bytes against.
3. Run the service
nixosModules.default exists from the first release after v0.0.22; pin that tag or a later one. A tag up to v0.0.22 has the package only.
Write the configuration next to the NixOS configuration that uses it. The smallest file that starts is one group named default and one enabled upstream:
.{
.groups = .{ .{ .name = "default" } },
.upstreams = .{ .{ .url = "https://cloudflare-dns.com/dns-query" } },
}
Pass the flake inputs to the module system, import the module, and enable it:
{ inputs, ... }:
{
imports = [ inputs.nxdns.nixosModules.default ];
services.nxdns = {
enable = true;
configFile = ./nxdns-config.zon;
openFirewall = true;
};
}
Then nixos-rebuild switch. The module has four options:
| Option | Default | What it does |
|---|---|---|
enable |
false |
Runs nxdns as nxdns.service, as the nxdns system user. |
package |
the release of the pinned tag | The nxdns release to run. |
configFile |
none, required | The configuration, in ZON. It is installed at /etc/nxdns/config.zon. |
openFirewall |
false |
Opens port 53 over UDP and TCP. The web interface on port 8080 and the DoH and DoT ports stay closed; open them yourself if you want them reachable from the network. |
configFile is copied into the Nix store, which every local user can read. Do not put a plaintext web.password in it: put the web.password_hash from an export there instead, as set up admin authentication describes.
What the module does with them:
- The build checks the configuration.
nxdns importfrom the pinnedpackagereadsconfigFileinto a throwaway database during the build. A file thatimportrejects, because it does not parse, breaks a field rule or does not reconcile, failsnixos-rebuildwith the binary's own reason, as inFAIL rules[0].pattern: ..., and nothing is deployed.nxdns checkis not used, because it queries every upstream and the build sandbox has no network. So the build does not load the DoH and DoT certificate and key files, and does not reach the upstreams: a missing certificate still stops the service at start. The build also says nothing about the database already on the machine; see the rollback note below. - nxdns runs in file mode. The service runs
nxdns run --config=/etc/nxdns/config.zon: every start reconciles the database onto the file, and the web interface refuses configuration edits. Change the file and switch to change anything. A switch that changes only the configuration restarts the service. - The unit is the packaged one. The package installs
deploy/systemd/nxdns.servicefrom the release tarball, with its sandboxing and itsRestartPreventExitStatus=2 64, and the module adds a drop-in that changes only the command line and leaves/etc/nxdnsto NixOS.systemctl cat nxdns.serviceshows both.
The module does not touch the machine's own resolver. If you point networking.nameservers at 127.0.0.1, the machine loses name resolution whenever nxdns is down, including while you repair it; a resolver outside the machine avoids that.
Rolling back. A nixos-rebuild --rollback or an older generation puts an older binary in front of a database that the newer one may have migrated. That binary refuses the database with SchemaTooNew, exit 2, and the unit stops instead of restarting. Upgrade describes the recovery.
The package alone is also available, for a machine that runs nxdns some other way: inputs.nxdns.packages.${pkgs.stdenv.hostPlatform.system}.default installs bin/nxdns, lib/systemd/system/nxdns.service, and LICENSE and THIRD-PARTY-NOTICES under share/doc/nxdns. It sets meta.mainProgram, so lib.getExe resolves to the binary.
4. Let Renovate bump the tag
Renovate's built-in nix manager does not do this. It only advances flake.lock along the ref an input already tracks, and a refs/tags/vX.Y.Z ref never moves, so it reports no releases for a tag-pinned input. Verified on 2026-09-09 with Renovate 42.99.0 against a Gitea host: all flake inputs, GitHub-hosted ones included, came back with an empty release list.
What works is a regex custom manager that treats the tag in the input URL as a version string, with the gitea-tags datasource:
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": ["/^flake\\.nix$/"],
"matchStrings": ["git\\+https://git\\.mial\\.net/mokhtar/nxdns\\.git\\?ref=refs/tags/(?<currentValue>v\\d+\\.\\d+\\.\\d+)"],
"depNameTemplate": "mokhtar/nxdns",
"datasourceTemplate": "gitea-tags",
"registryUrlTemplate": "https://git.mial.net",
"versioningTemplate": "semver"
}
]
}
Verified on 2026-09-09: with this manager, Renovate opened the pull request for v0.0.18 on the day of the tag. That pull request rewrites the tag in flake.nix only. flake.lock still records the old revision, so the same pull request must also refresh the lock, either with a postUpgradeTasks command (nix flake update nxdns, which needs the self-hosted allowedCommands setting) or with a CI job on the Renovate branch that commits the lock. The datasource proposes only tags that already exist, so it never pins a release that has not been cut.
Between the tag push and the asset upload there is a window in which the tag exists and the release tarballs do not. A pin written by hand during that window evaluates, then fails at build time with a 404 from the release download URL. Wait for the release to be published, or re-run the build once it is. Renovate's own pull requests are not affected by the window in practice, because it runs on a schedule rather than on the tag push.
Upgrading
Change the ?ref=refs/tags/vX.Y.Z in the input, run nix flake lock --update-input nxdns, and rebuild. Read the release notes first: nxdns is pre-0.1 and breaks on purpose, and upgrade lists what state a version change touches.