# syntax=docker/dockerfile:1.7 # # Two stages: build the console, then build a static Go binary with it # embedded. The result is a single file with no runtime dependencies beyond # libc, which is what makes a scratch/distroless base safe. ARG NODE_IMAGE=node:22-alpine ARG GO_IMAGE=golang:1.27-alpine ARG RUNTIME_IMAGE=gcr.io/distroless/static-debian12:nonroot # ---- stage 1: the admin console ------------------------------------------- FROM ${NODE_IMAGE} AS web WORKDIR /src/web # Dependencies first: this layer is only rebuilt when package-lock.json moves, # not on every source edit. COPY web/package.json web/package-lock.json* ./ RUN npm ci --no-audit --no-fund COPY web/ ./ # vite.config.ts writes to ../internal/web/dist, which is where the Go embed # directive looks. RUN mkdir -p /src/internal/web && npm run build \ && test -f /src/internal/web/dist/index.html # ---- stage 2: the binary --------------------------------------------------- FROM ${GO_IMAGE} AS build ARG VERSION=dev ARG TARGETOS=linux ARG TARGETARCH=amd64 WORKDIR /src # Download modules before copying the application sources. COPY go.mod go.sum ./ RUN go mod download COPY . . # Copy the fresh bundle last: the source tree contains a tracked index.html # whose hashed asset references can be stale. It must never overwrite the # index generated alongside the JavaScript and CSS in the web stage. COPY --from=web /src/internal/web/dist/ internal/web/dist/ # CGO off: the resolver, bbolt and the TLS stack are all pure Go, and a static # binary is what lets the runtime stage be distroless-static. RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -trimpath \ -ldflags "-s -w -X git.k8s-git-k6n.net/mats/fancy-dns/internal/app.Version=${VERSION}" \ -o /out/fancy-dns ./cmd/fancy-dns # The console is a separate binary in the same image: run it with # `command: ["/usr/local/bin/fancy-dns-console"]`. It shares the embedded UI. RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -trimpath \ -ldflags "-s -w -X git.k8s-git-k6n.net/mats/fancy-dns/internal/app.Version=${VERSION}" \ -o /out/fancy-dns-console ./cmd/fancy-dns-console # ---- stage 3: runtime ------------------------------------------------------ FROM ${RUNTIME_IMAGE} # 53 is the DNS service itself, 853 is DoT, 443 is DoH, 8080 is the console. EXPOSE 53/udp 53/tcp EXPOSE 853/tcp EXPOSE 443/tcp EXPOSE 8080/tcp # The bbolt files and downloaded lists live here; mount a volume over it. VOLUME ["/var/lib/fancy-dns"] COPY --from=build /out/fancy-dns /usr/local/bin/fancy-dns COPY --from=build /out/fancy-dns-console /usr/local/bin/fancy-dns-console COPY --from=build /src/deploy/config.example.yaml /etc/fancy-dns/config.example.yaml COPY --from=build /src/deploy/zones/ /etc/fancy-dns/zones/ USER nonroot:nonroot # The liveness probe, not the readiness one: it answers 200 while the process # is up, which is what a crash-loop detector wants. HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD ["/usr/local/bin/fancy-dns", "-version"] ENTRYPOINT ["/usr/local/bin/fancy-dns"] CMD ["-config", "/etc/fancy-dns/config.yaml"]