# Lint phase. The linter is invoked directly rather than through `make # lint` or `script/lint`, which are themselves a docker build and would # recurse into a daemon that does not exist in a build step. # # golangci/golangci-lint v2.12.2 (built with go1.26.2), 2026-05-06 FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN golangci-lint run --config .golangci.yml ./... # Test phase, same shape and for the same reason. The go directive in # go.mod is a minimum, so this Go may be newer than the linter's. The # Debian image rather than the Alpine one, because the race detector # needs the C compiler it carries. # # golang 1.27.1-trixie, 2026-09-19 FROM golang@sha256:3b77fc618ec235a1ab412de7737f120dd507c57e8d87de4cbb7994fb94275ed5 AS test WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go test -count=1 -timeout 90s -race -cover ./... || \ { echo "--- Rerunning with -v for details ---"; \ go test -count=1 -timeout 90s -race -v ./...; exit 1; } # Build stage. Nothing is wanted from the two phases above; the copies # are what make BuildKit build them first, so the image, which needs this # stage, cannot be produced unless lint and test passed. # # golang 1.27.1-trixie, 2026-09-19 FROM golang@sha256:3b77fc618ec235a1ab412de7737f120dd507c57e8d87de4cbb7994fb94275ed5 AS builder COPY --from=lint /src/go.sum /dev/null COPY --from=test /src/go.sum /dev/null WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . # The version is computed on the host and passed in, because # .dockerignore excludes .git. ARG VERSION=dev RUN CGO_ENABLED=0 go build -trimpath \ -ldflags="-s -w -X main.Version=${VERSION}" \ -o /usr/local/bin/smallwebwaf ./cmd/smallwebwaf # runsvinit, the image's entrypoint, built at the last commit of its # archived repository. It has no go.mod, and `go build` of its directory # needs one; it uses only the standard library, so the one written here # names nothing else. # # golang 1.27.1-trixie, 2026-09-19 FROM golang@sha256:3b77fc618ec235a1ab412de7737f120dd507c57e8d87de4cbb7994fb94275ed5 AS runsvinit RUN git clone --quiet https://github.com/peterbourgon/runsvinit /src WORKDIR /src RUN git checkout --quiet --detach b4b2c785308b1ce785b6155c7fe5f16879080193 \ && go mod init github.com/peterbourgon/runsvinit \ && CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \ -o /usr/local/bin/runsvinit . # The image an app's Dockerfile builds FROM, described under "Deployment" # in SPEC.md. It is the last stage, so a plain `docker build .` builds it. # # ubuntu 26.04, 2026-09-27 FROM ubuntu@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7 # ca-certificates, nix-bin and runit, from Ubuntu's archive as it was at # the snapshot moment, which is never earlier than the Ubuntu image above. # apt checks every package against the snapshot's InRelease files, and # this step checks those against the hashes named here, which are those # of the amd64 archive: other architectures use Ubuntu's ports archive. # apt also fetches the live archive's InRelease files, which change daily # and which the install does not use. The snapshot service is HTTPS only # and this image has no CA certificates yet, so this step uses the Go # image's. RUN --mount=type=bind,from=builder,source=/etc/ssl/certs/ca-certificates.crt,target=/tmp/go-image-ca.crt \ apt-get update --snapshot 20261001T000000Z \ -o Acquire::https::CaInfo=/tmp/go-image-ca.crt \ && printf '%s\n' \ '45f95ce276cdba3e41870516a130e03c58b8b7a79e9546b0efe9e526d255740c snapshot.ubuntu.com_ubuntu_20261001T000000Z_dists_resolute_InRelease' \ '802e675dd9de4c7f3916434a95e7c1d8eec0e82886622d7805ab19a2c6fe0365 snapshot.ubuntu.com_ubuntu_20261001T000000Z_dists_resolute-updates_InRelease' \ '64b3353f0bd4970b4f7271962245bcea9ff24d4cc7bea16b433f8a60e42ca3dd snapshot.ubuntu.com_ubuntu_20261001T000000Z_dists_resolute-backports_InRelease' \ '1d5041572116a8b23aabf79ac7439ad8af83d57ad3fb0f9aa0d4523ec10c5908 snapshot.ubuntu.com_ubuntu_20261001T000000Z_dists_resolute-security_InRelease' \ | (cd /var/lib/apt/lists && sha256sum --check --strict) \ && DEBIAN_FRONTEND=noninteractive apt-get install --yes --no-install-recommends \ --snapshot 20261001T000000Z \ -o Acquire::https::CaInfo=/tmp/go-image-ca.crt \ ca-certificates nix-bin runit \ && rm -rf /var/lib/apt/lists/* # Nix run by root expects a group of build users, which nix-bin does not # create; with the setting empty, root's builds run without them. RUN mkdir /etc/nix && echo 'build-users-group =' > /etc/nix/nix.conf # nixpkgs, from its release file, checked by SHA-256, and set up for root # as `nixpkgs`, so that an app's Dockerfile installs a package with # `nix-env -iA nixpkgs.`. curl and xz come with nix-bin. RUN curl -fsSL -o /tmp/nixexprs.tar.xz \ https://releases.nixos.org/nixos/26.05/nixos-26.05.11045.774debe7a0d1/nixexprs.tar.xz \ && echo 'b2994104605601690023a5a6a3bb5a07b2bd1716b4e3b208cba1056dacd2ab08 /tmp/nixexprs.tar.xz' \ | sha256sum --check --strict \ && mkdir -p /root/.nix-defexpr/nixpkgs \ && tar -xJf /tmp/nixexprs.tar.xz -C /root/.nix-defexpr/nixpkgs --strip-components=1 \ && rm /tmp/nixexprs.tar.xz # What root installs with nix-env lands in root's profile. This path to # it works for every user, unlike /root/.nix-profile: only root can # enter /root. It comes last, so that no package shadows the image's # own tools: busybox, for one, brings an sv that looks for services # elsewhere. ENV PATH=${PATH}:/nix/var/nix/profiles/default/bin COPY --from=runsvinit /usr/local/bin/runsvinit /usr/local/bin/runsvinit COPY --from=builder /usr/local/bin/smallwebwaf /usr/local/bin/smallwebwaf RUN groupadd --system --gid 65532 smallwebwaf \ && useradd --system --uid 65532 --gid smallwebwaf --no-create-home \ --shell /usr/sbin/nologin smallwebwaf # runsvinit starts runit's runsvdir on /etc/service, where Ubuntu's sv # looks too. COPY --chmod=755 share/smallwebwaf.run /etc/service/smallwebwaf/run EXPOSE 8080 # traefik sends a container no requests until it is healthy, so the # check runs every second from the start until it first passes, for up # to a minute, and every 30 seconds after that. HEALTHCHECK --start-period=1m --start-interval=1s \ CMD ["/usr/local/bin/smallwebwaf", "healthcheck"] ENTRYPOINT ["/usr/local/bin/runsvinit"]