milestone 14: build, package, sign and publish releases
This commit is contained in:
@@ -1,53 +1,85 @@
|
||||
# Install nxdns with Docker
|
||||
|
||||
Builds the nxdns image and runs it with Docker Compose. At the end a container
|
||||
Runs the published nxdns image with Docker Compose. At the end a container
|
||||
answers DNS on port 53 and keeps its data in a named volume.
|
||||
|
||||
The image is multi-architecture — `linux/amd64` and `linux/arm64` — so the same
|
||||
tag works on a PC and on a Raspberry Pi 5. Building the image yourself is still
|
||||
supported and is the last section of this page.
|
||||
|
||||
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.
|
||||
> Verification: the seed-file failure modes, the run and the two checks in
|
||||
> step 3 were run on the machine that wrote this page, against an image built
|
||||
> from this checkout rather than pulled from the registry — no release is
|
||||
> published yet, so nothing on this page could be run against a pulled image,
|
||||
> and step 1 could not be run at all. One command was run in altered form:
|
||||
> host port 8080 was occupied here, so the run and the 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. The `chown` to uid 65532 needs
|
||||
> root and was not run.
|
||||
|
||||
## 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:
|
||||
## 1. Pull and verify the image
|
||||
|
||||
```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 .
|
||||
BASE=https://git.mial.net/mokhtar/nxdns
|
||||
VERSION=$(curl -fsS -o /dev/null -w '%{redirect_url}' "$BASE/releases/latest" |
|
||||
sed 's#.*/releases/tag/v##')
|
||||
docker pull git.mial.net/mokhtar/nxdns:$VERSION
|
||||
```
|
||||
|
||||
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.
|
||||
Pin a version. `:latest` exists and moves, which is what you want when you are
|
||||
trying it out and not what you want on a machine your household's DNS depends
|
||||
on. The lookup above asks the server for the current release rather than
|
||||
hardcoding a number that goes stale one release later — Gitea redirects
|
||||
`releases/latest` to the newest published release's tag page. To take a
|
||||
particular version instead, set `VERSION=<version>` yourself.
|
||||
|
||||
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:
|
||||
Verify what you pulled before you run it. The release publishes an
|
||||
`IMAGE-DIGEST.txt` asset naming the digest of the image index, and that file is
|
||||
covered by the signed `SHA256SUMS.txt`:
|
||||
|
||||
```sh
|
||||
docker compose -f deploy/docker/compose.yaml build
|
||||
curl -fLO "$BASE/releases/download/v$VERSION/IMAGE-DIGEST.txt"
|
||||
curl -fLO "$BASE/releases/download/v$VERSION/SHA256SUMS.txt"
|
||||
curl -fLO "$BASE/releases/download/v$VERSION/SHA256SUMS.txt.asc"
|
||||
gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt
|
||||
sha256sum -c --ignore-missing SHA256SUMS.txt
|
||||
docker buildx imagetools inspect git.mial.net/mokhtar/nxdns:$VERSION \
|
||||
--format '{{.Manifest.Digest}}'
|
||||
cut -d@ -f2 IMAGE-DIGEST.txt
|
||||
```
|
||||
|
||||
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`.
|
||||
The last two have to print the same string — `IMAGE-DIGEST.txt` holds a whole
|
||||
pinned reference, `name:tag@sha256:…`, so the `cut` is what reduces it to the
|
||||
digest `imagetools` prints. [Verify a release](verify-a-release.md)
|
||||
covers the key, the fingerprint, every failure message, and what the signature
|
||||
does and does not prove.
|
||||
|
||||
## 2. Write the seed configuration
|
||||
> Not verified on this host: no image and no release are published yet, so
|
||||
> `docker pull` and every URL here fail today, and the `releases/latest` lookup
|
||||
> returns 404 and leaves `VERSION` empty. That lookup was run against
|
||||
> `gitea.com/gitea/tea` on Gitea `1.27.0+dev` and printed `0.15.1`. The
|
||||
> `docker buildx imagetools inspect --format` shape was run here against
|
||||
> `alpine:3.22` on Docker Hub and printed that image's index digest.
|
||||
|
||||
## 2. Get the compose file and write the seed configuration
|
||||
|
||||
Every path on this page is relative to a checkout of the repository, because
|
||||
that is how it was verified. Running the published image needs no checkout,
|
||||
though — one file is enough. Fetch it for the version you pulled and work in
|
||||
its directory instead, dropping `deploy/docker/` from the paths below:
|
||||
|
||||
```sh
|
||||
mkdir -p ~/nxdns && cd ~/nxdns
|
||||
curl -fLO "$BASE/raw/tag/v$VERSION/deploy/docker/compose.yaml"
|
||||
```
|
||||
|
||||
> Not verified against nxdns: there is no tag yet. The Gitea raw-file URL shape
|
||||
> `<repo>/raw/tag/<tag>/<path>` was run here against `gitea.com/gitea/tea` on
|
||||
> Gitea `1.27.0+dev` and returned the file with a 200.
|
||||
|
||||
Compose bind-mounts `deploy/docker/etc-nxdns` read-only at `/etc/nxdns`. Create
|
||||
it and put the seed file in it:
|
||||
@@ -75,8 +107,8 @@ 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`:
|
||||
diagnostic and exits 2 as well. Both were run here against a locally built
|
||||
image. A seed file whose only group was named `other`:
|
||||
|
||||
```
|
||||
FAIL groups: no group named 'default'; every unknown client is assigned to it
|
||||
@@ -115,10 +147,28 @@ chmod 0600 deploy/docker/etc-nxdns/config.zon
|
||||
## 3. Run it
|
||||
|
||||
```sh
|
||||
docker compose -f deploy/docker/compose.yaml up -d
|
||||
NXDNS_VERSION=$VERSION docker compose -f deploy/docker/compose.yaml up -d
|
||||
docker compose -f deploy/docker/compose.yaml logs -f
|
||||
```
|
||||
|
||||
`compose.yaml` reads the image from two variables:
|
||||
`${NXDNS_IMAGE:-git.mial.net/mokhtar/nxdns:${NXDNS_VERSION:-latest}}`. Set
|
||||
`NXDNS_VERSION` to pin a release; set `NXDNS_IMAGE` to run something else
|
||||
entirely, which is what the build-from-source section at the bottom does.
|
||||
|
||||
> Verified on this host with `docker compose -f deploy/docker/compose.yaml
|
||||
> config`, which resolves the variables without contacting a registry: no
|
||||
> variables gives `git.mial.net/mokhtar/nxdns:latest`, `NXDNS_VERSION=0.0.1`
|
||||
> gives `git.mial.net/mokhtar/nxdns:0.0.1`, and `NXDNS_IMAGE=nxdns` gives
|
||||
> `nxdns`.
|
||||
|
||||
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 `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`.
|
||||
|
||||
A healthy first start logs the seeding and the bound sockets:
|
||||
|
||||
```
|
||||
@@ -141,7 +191,8 @@ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
|
||||
> 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.
|
||||
> host port is occupied; free the port or edit the `ports:` list. The image
|
||||
> under test was built locally, not pulled: there is nothing published to pull.
|
||||
|
||||
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
|
||||
@@ -157,31 +208,81 @@ 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
|
||||
## 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:
|
||||
Nothing changes. The published tag is a multi-architecture index, so
|
||||
`docker pull` on the Pi selects the `linux/arm64` image on its own. The
|
||||
platform list is one of the things
|
||||
[Verify a release](verify-a-release.md) has you check.
|
||||
|
||||
To pull the arm64 image from an x86_64 machine — to inspect it, or to save and
|
||||
copy it — name the platform:
|
||||
|
||||
```sh
|
||||
docker pull --platform linux/arm64 git.mial.net/mokhtar/nxdns:$VERSION
|
||||
```
|
||||
|
||||
> Not verified on this host: nothing is published to pull, and this host is
|
||||
> x86_64 with no emulation, so an arm64 image could not be started here even
|
||||
> if it were.
|
||||
|
||||
## Build the image from source instead
|
||||
|
||||
The Dockerfile does not compile anything. It assembles a filesystem around
|
||||
binaries you build first, so build the admin interface and the release
|
||||
artifacts from the repository root:
|
||||
|
||||
```sh
|
||||
(cd web && npm ci && npm run build)
|
||||
VERSION=$(sed -n 's/^[[:space:]]*\.version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' build.zig.zon)
|
||||
zig build dist -Dversion-string="$VERSION" -Dgit-commit="$(git rev-parse HEAD)" \
|
||||
-Dweb-dist=web/dist -Doptimize=ReleaseSafe
|
||||
DOCKER_BUILDKIT=1 docker build -t nxdns -f deploy/docker/Dockerfile .
|
||||
```
|
||||
|
||||
Take the version from `build.zig.zon` rather than inventing one: `verify-dist`
|
||||
asserts the two agree, so a made-up string builds but fails verification.
|
||||
BuildKit is required — the Dockerfile pins its builder stage to
|
||||
`$BUILDPLATFORM`, which the classic builder does not define.
|
||||
|
||||
Build `web/dist` every time, before the binaries. A stale bundle is embedded
|
||||
silently and ships an admin interface that does not match its API — which is
|
||||
why `dist` refuses to build against the `web/dist-placeholder` default at all.
|
||||
|
||||
The context has to be the repository root, because the Dockerfile copies
|
||||
`zig-out/dist/bin` and `zig-out/dist/stage`. The result is a `scratch` image
|
||||
holding the binary, a CA bundle, `/LICENSE`, `/THIRD-PARTY-NOTICES` and two
|
||||
empty directories.
|
||||
|
||||
Run that image instead of the published one by naming it:
|
||||
|
||||
```sh
|
||||
NXDNS_IMAGE=nxdns docker compose -f deploy/docker/compose.yaml up -d
|
||||
```
|
||||
|
||||
For an arm64 image on an x86_64 machine, use buildx. The Dockerfile's builder
|
||||
stage is pinned to `$BUILDPLATFORM` and only copies files, so no emulation is
|
||||
involved:
|
||||
|
||||
```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.
|
||||
Add `--push` or `--load` to keep the result; 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.
|
||||
> Verified on this host, except the two buildx lines. `zig build dist` was run
|
||||
> to completion with the version read out of `build.zig.zon` and exited 0, and
|
||||
> the `DOCKER_BUILDKIT=1 docker build` above was then run against that
|
||||
> `zig-out/dist` tree and exited 0. The binary copied out of the resulting
|
||||
> image with `docker cp` hashed identically to
|
||||
> `zig-out/dist/stage/nxdns-<version>-x86_64-linux-musl/nxdns`. The
|
||||
> `NXDNS_IMAGE=nxdns docker compose ... up -d` line was verified as described in
|
||||
> step 3, with the host ports moved. `docker buildx build --platform
|
||||
> linux/arm64` was run here too and exited 0 on an x86_64 host with no
|
||||
> emulation available — the builder stage is pinned to `$BUILDPLATFORM` and the
|
||||
> final stage is `FROM scratch`, so nothing arm64 ever executes during the
|
||||
> build. It printed the driver's `No output specified` warning, which is the
|
||||
> reason the paragraph above tells you to add `--push` or `--load`. See
|
||||
> [Install with systemd](install-with-systemd.md#build-from-source-instead) for
|
||||
> the `dist` and `verify-dist` detail.
|
||||
|
||||
Reference in New Issue
Block a user