Skip to content

Install with Nix

Add nxdns to a NixOS machine as a flake input pinned to a release, and run it with the flake’s services.nxdns module. At the end the machine answers DNS on port 53 from a configuration file that was checked when the system was built.

The flake does not build nxdns from source. Its package fetches the release tarball for x86_64-linux or aarch64-linux and pins its SHA-256, so a changed byte fails the build.

1. Add the input

In your flake:

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nxdns = {
url = "git+https://git.mial.net/mokhtar/nxdns.git?ref=refs/tags/v0.0.25";
inputs.nixpkgs.follows = "nixpkgs";
};
};

Pin a tag, not a branch: a tag never moves under you. The follows line saves fetching and evaluating a second nixpkgs. Then write the lock entry:

Terminal window
nix flake lock

2. Enable the service

Write the configuration next to your NixOS configuration, for example as nxdns-config.zon. The smallest one that starts is a group named default and one upstream resolver:

.{
.groups = .{ .{ .name = "default" } },
.upstreams = .{ .{ .url = "https://cloudflare-dns.com/dns-query" } },
}

Import the module and enable it:

{ inputs, ... }:
{
imports = [ inputs.nxdns.nixosModules.default ];
services.nxdns = {
enable = true;
configFile = ./nxdns-config.zon;
openFirewall = true;
};
}

Then run nixos-rebuild switch.

OptionDefaultWhat it does
enablefalseRuns nxdns as nxdns.service, as the nxdns system user.
packagethe release of the pinned tagThe nxdns release to run.
configFilenone, requiredThe configuration, installed as /etc/nxdns/config.zon.
openFirewallfalseOpens port 53 over UDP and TCP. The web interface (8080) and the DoH and DoT ports stay closed; open them yourself if other machines should reach them.

configFile is copied into the Nix store, which every local user can read. Do not put a plaintext web.password in it. Use the web.password_hash from an export instead, as Set up admin authentication shows.

What the module does:

  • The build checks the configuration. The pinned nxdns loads configFile into a throwaway database during the build, so a file nxdns would reject fails nixos-rebuild with nxdns’s own message, for example FAIL rules[0].pattern: '(' is not a valid regex pattern, and nothing is deployed. The build has no network, so it cannot check that the upstreams answer or load DoH and DoT certificates; a missing certificate still stops the service when it starts.
  • nxdns runs in file mode. Every start applies the file, and the web interface shows the configuration read-only. To change anything, change the file and switch. A switch that changes only the configuration restarts the service.
  • The unit is the packaged one, with its sandboxing. The module only replaces its command line; systemctl cat nxdns.service shows both.

The module does not touch the machine’s own resolver. Point your network at nxdns by setting the DNS server your router hands out to this machine’s address, and leave networking.nameservers on another resolver, so the machine can still resolve names while nxdns is down.

The package alone is inputs.nxdns.packages.${pkgs.stdenv.hostPlatform.system}.default, for running nxdns some other way. It installs bin/nxdns and lib/systemd/system/nxdns.service, and sets meta.mainProgram.

3. Upgrade

Read the release notes, change the tag in the input URL, then:

Terminal window
nix flake lock --update-input nxdns
nixos-rebuild switch

Rolling back to an older generation puts an older nxdns in front of a database the newer one may already have upgraded. The older binary then refuses to start; Upgrade explains the way back.

Let Renovate bump the tag

Renovate’s built-in nix manager cannot do this: it only moves flake.lock along the ref an input already follows, and a tag ref never moves. A regex custom manager that reads the tag from the input URL, with the gitea-tags datasource, does:

{
"$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"
}
]
}

Its pull request changes the tag in flake.nix only. The same pull request also needs a refreshed flake.lock: run nix flake update nxdns as a postUpgradeTasks command (self-hosted Renovate, with allowedCommands), or have CI commit the lock on the Renovate branch.

A tag can exist for a short while before its release files are uploaded. A pin made by hand in that window fails the build with a 404 from the download URL; build again once the release is published.