7.4 KiB
Install nxdns with Docker
Builds the nxdns image and runs it with Docker Compose. At the end a container answers DNS on port 53 and keeps its data in a named volume.
For what each configuration field means, see the configuration reference.
Verification: every command on this page was run on the machine that wrote it, with three exceptions marked below — the
chownto uid 65532 needs root, the arm64 image was built but not run, and pushing to a registry needs credentials. One command was run in altered form: host port 8080 was occupied here, so the run and the two verification commands in step 3 were executed with the host side of the port mappings moved to 25353 and 28088 rather than the 53 and 8080 printed below. The container side was unchanged. See the note in step 3.
1. Build the image
The Dockerfile does not compile anything. It assembles a filesystem around a binary you build first, so build the admin interface and the binaries from the repository root:
(cd web && npm ci && npm run build)
zig build cross -Dweb-dist=web/dist -Doptimize=ReleaseSafe
docker build -t nxdns -f deploy/docker/Dockerfile .
Build web/dist every time, before the binary. A stale bundle is embedded
silently and ships an admin interface that does not match its API.
The context has to be the repository root, because the Dockerfile copies
zig-out/cross. The result is a scratch image holding the binary, a CA
bundle and two empty directories — 28.2 MB here.
Compose runs the same build with the right context:
docker compose -f deploy/docker/compose.yaml build
Every block on this page runs from the repository root, and none of them change
directory, so they can be pasted in order. -f is what makes that work:
Compose resolves the relative paths inside compose.yaml — the build context,
the etc-nxdns bind mount — against the directory holding the file, not
against your shell, and it takes the project name docker from that directory
either way, which is why the container is docker-nxdns-1.
2. Write the seed configuration
Compose bind-mounts deploy/docker/etc-nxdns read-only at /etc/nxdns. Create
it and put the seed file in it:
mkdir -p deploy/docker/etc-nxdns
$EDITOR deploy/docker/etc-nxdns/config.zon
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" } },
.web = .{ .password = "choose-a-real-password" },
}
Without that file the container exits with code 2 on a fresh volume: an empty
database has nothing to forward to. The log is
no configuration file at '/etc/nxdns/config.zon'; using the database as it is
followed by nxdns run failed: NoUsableUpstreams.
A file that is present but rejected is a different failure with the same exit
code. No default group, no enabled upstream, a syntax error — run prints the
diagnostic and exits 2 as well. Both were run here against this image. A seed
file whose only group was named other:
FAIL groups: no group named 'default'; every unknown client is assigned to it
nxdns run failed: MissingDefaultGroup
run `nxdns check` to see the configuration in full
and an empty /etc/nxdns:
info(config_bootstrap): no configuration file at '/etc/nxdns/config.zon'; using the database as it is
nxdns run failed: NoUsableUpstreams
run `nxdns check` to see the configuration in full
Under restart: unless-stopped either one is a restart loop, and the exit code
alone no longer tells them apart: read the lines above the failure, which either
name the diagnostic in the file or say there was no file at all. See
Troubleshoot nxdns.
The container runs as uid 65532, and the mount is read-only, so the container
cannot repair permissions itself. Mode 0644 works and was used here. If the
file carries a secret — web.password, or a web.password_hash from a
restored export — give it to that uid instead:
chown 65532:65532 deploy/docker/etc-nxdns/config.zon
chmod 0600 deploy/docker/etc-nxdns/config.zon
Not verified on this host:
chownto a uid you do not own needs root. What was verified is the failure it prevents — a seed file at 0600 owned by another uid makes the container lognxdns run failed: AccessDeniedand restart in a loop. See Troubleshoot nxdns.
3. Run it
docker compose -f deploy/docker/compose.yaml up -d
docker compose -f deploy/docker/compose.yaml logs -f
A healthy first start logs the seeding and the bound sockets:
info(config_bootstrap): seeded the database from '/etc/nxdns/config.zon'
info(nxdns): nxdns <version> serving on udp [::]:53 tcp [::]:53 tcp 0.0.0.0:53; 1 upstream(s); blocklist generation 1
info(web_server): web interface listening on 0.0.0.0:8080
Confirm it answers and that the admin interface is up:
dig @127.0.0.1 example.com A +short
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
On the machine that wrote this page, host port 8080 was already taken by an unrelated process, so the container was verified with the host side of the port mappings moved to 25353 and 28088 — everything inside the container was unchanged, and the log still reads
serving on udp [::]:53. Against those portsdigreturned the A records forexample.comandcurlreturned 200.docker compose up -dfails withfailed to bind host port 0.0.0.0:8080/tcp: address already in usewhen a host port is occupied; free the port or edit theports:list.
The compose file publishes 53/udp, 53/tcp and 8080, keeps /var/lib/nxdns in
the named volume nxdns-data, and sets the per-namespace sysctl
net.ipv4.ip_unprivileged_port_start=0 so uid 65532 can bind port 53 without
any capability. Uncomment the 443 and 853 mappings when you enable the DoH or
DoT listener; see Enable DoH and DoT.
4. Do not point the host at the container
The container resolves its own upstream DoH and DoT hostnames through the
host's DNS configuration. If you set the host's /etc/resolv.conf to the nxdns
container, the container's startup lookups depend on the service that is trying
to start. Point LAN clients at nxdns; leave the container's host on its own
resolver.
Build for a Raspberry Pi 5
The Dockerfile maps buildx's TARGETARCH onto the cross-target directory, so
the aarch64 image comes from the same zig-out/cross tree with no second
compile. Under the legacy builder, where TARGETARCH is empty, the Dockerfile
falls back to the build host's uname -m, so a plain docker build on the Pi
itself also selects the aarch64 binary:
docker buildx build --platform linux/arm64 -t nxdns:arm64 -f deploy/docker/Dockerfile .
This was run here and completed; add --push or --load to keep the result,
since the default buildx driver leaves it in the build cache.
Not verified on this host: the arm64 image was not started. Running it needs an aarch64 machine or qemu binfmt emulation, neither of which is available here.
Publish the image to a registry
There is no registry push in CI on purpose: credentials and the choice of
registry are infrastructure decisions, not this repository's. Publish by hand
with docker login <registry>, then docker tag nxdns <registry>/<owner>/nxdns:<tag>, then docker push <registry>/<owner>/nxdns:<tag>.
Not verified on this host: pushing needs credentials for a registry.