# 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](../reference/configuration.md). > Verification: every command on this page was run on the machine that wrote > it, with three exceptions marked below — the `chown` to 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: ```sh (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: ```sh 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: ```sh 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: ```zon .{ .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](troubleshoot.md). 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: ```sh chown 65532:65532 deploy/docker/etc-nxdns/config.zon chmod 0600 deploy/docker/etc-nxdns/config.zon ``` > Not verified on this host: `chown` to 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 log `nxdns run failed: AccessDenied` and > restart in a loop. See [Troubleshoot nxdns](troubleshoot.md). ## 3. Run it ```sh 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 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: ```sh 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 > ports `dig` returned the A records for `example.com` and `curl` returned 200. > `docker compose up -d` fails with > `failed to bind host port 0.0.0.0:8080/tcp: address already in use` when a > host port is occupied; free the port or edit the `ports:` 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](enable-doh-and-dot.md). ## 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: ```sh 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 `, then `docker tag nxdns //nxdns:`, then `docker push //nxdns:`. > Not verified on this host: pushing needs credentials for a registry.