diff --git a/Dockerfile b/Dockerfile index 2885086..98a2174 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,14 +51,21 @@ RUN echo "gate lint, epoch ${CHECK_EPOCH}" && \ FROM golang@sha256:56961d79ea8129efddcc0b8643fd8a5416b4e6228cfd477e3fd61deb2672c587 AS builder # We never build or run as root. Create an unprivileged user and point -# HOME and the Go caches at its home so go build and go test can write -# their caches when we drop to it below. $GOPATH/bin is deliberately not -# on PATH: script/bootstrap no longer `go install`s anything (the linter -# runs from a pinned image, never from a host install), so nothing lands +# HOME and the build cache at its home so go build and go test can write +# it when we drop to it below. $GOPATH/bin is deliberately not on PATH: +# script/bootstrap no longer `go install`s anything (the linter runs +# from a pinned image, never from a host install), so nothing lands # there and adding it would only widen what this image resolves. +# +# The module cache is kept outside that home, at the base image's +# default /go/pkg/mod, and belongs to root: script/bootstrap fills it as +# root. Do not move it into the home and hand it over with `chown -R`: +# that walks every file in it, which took from about 80 s to over ten +# minutes on a shared host, depending on load. RUN adduser -D -u 1000 builder ENV HOME=/home/builder ENV GOPATH=/home/builder/go +ENV GOMODCACHE=/go/pkg/mod ENV GOCACHE=/home/builder/.cache/go-build WORKDIR /src @@ -83,11 +90,22 @@ COPY script/ script/ COPY go.mod go.sum ./ RUN script/bootstrap -COPY . . +# Hand builder only what it writes to, without walking the module cache. +# This layer stays cached with bootstrap. +# - /src itself: make build writes the binary into it, and git refuses +# a repository whose top directory belongs to another user. +# - the module cache's cache/download directory itself, not what is in +# it: Go only reads the downloaded modules, but make build saves its +# lookup of this module's own version from git there, in a new +# directory named after the module path. +# - builder's home: the go commands bootstrap ran as root left Go's +# telemetry files there, a few small files. +RUN chown builder:builder /src /go/pkg/mod/cache/download && \ + chown -R builder:builder /home/builder -# Hand the sources and caches to the unprivileged user, then drop root -# before running any checks or builds. -RUN chown -R builder:builder /src /home/builder +# The sources are handed to builder as they are copied, so no layer has +# to walk them. Then drop root before running any checks or builds. +COPY --chown=builder:builder . . USER builder # Fail the build unless the branch is green. Runs as non-root so the diff --git a/TODO.md b/TODO.md index 7e77fb3..03b7b68 100644 --- a/TODO.md +++ b/TODO.md @@ -29,6 +29,10 @@ # Completed Steps +- the `Dockerfile` build stage keeps the Go module cache out of `builder`'s + home and copies the sources with `--chown`, so no `chown -R` walks them + (2026-10-04, https://git.eeqj.de/sneak/sfdupes/issues/43) + - `--version` prints `sfdupes VERSION` to stdout; README documents it and `--help` (2026-10-04, https://git.eeqj.de/sneak/sfdupes/issues/15)