# Lint stage # # This FROM line is the single source of truth for the linter version: # script/lint parses the image reference out of it and runs that exact # image, so a local `make lint` and CI use the same linter. Bump the # linter here (tag AND digest) and nowhere else. # # golangci/golangci-lint:v2.12.2-alpine, 2026-08-07 FROM golangci/golangci-lint:v2.12.2-alpine@sha256:91b27804074a0bacea298707f016911e60cf0cdbc6c7bf5ccacb5f0606d18d60 AS lint RUN apk add --no-cache make build-base # The context signal for script/lint's native path. This stage runs # `make lint` with no docker daemon available, so it is the one place # that must run the golangci-lint on PATH directly. script/lint takes # that path only when this is set AND the version matches the pin above; # version equality alone would also admit a developer's locally # installed copy on a host, bypassing the digest pin (issue #80). # Nothing outside this stage sets it. ENV VAULTIK_LINT_IN_CONTAINER=1 WORKDIR /src # Copy go mod files first for better layer caching COPY go.mod go.sum ./ RUN go mod download # Copy source code COPY . . # Run formatting check and linter. # # CHECK_EPOCH must stay immediately above these RUNs. These layers are # keyed on its value, so they are cache-eligible only for a value # already built against this same tree. script/cibuild and script/docker # each pass a fresh value on every invocation, which is what makes their # green mean the checks really executed. # # The value is expanded into each check command rather than left to a # bare declaration, so the cache miss does not depend on BuildKit's # unreferenced-ARG handling staying as it is. It also puts the epoch in # the build log, where a reader can see the layer was keyed fresh. # # The guard is what makes a build that omits --build-arg fail instead of # lie. An unset ARG is an empty string, and an empty string is a # perfectly stable cache key: without the guard the first such build # runs the checks and every one after it on an unchanged tree replays # these layers from cache, executes nothing, and still exits 0. Failed # steps are never cached, so the guard fails on EVERY invocation rather # than once -- a bare `docker build .` is now a loud error, not a quiet # green. Do not give CHECK_EPOCH a default value; a default would # satisfy the guard with a constant and restore the hole. # # ARG scope is per-stage, so the builder stage declares its own. # Everything above this line (apk, go.mod, `go mod download`) is # deliberately outside the busted range and keeps caching. ARG CHECK_EPOCH RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN echo "check epoch: ${CHECK_EPOCH}" && make fmt-check RUN echo "check epoch: ${CHECK_EPOCH}" && make lint # Build stage # golang:1.26.1-alpine, 2026-03-17 FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder # Depend on lint stage passing COPY --from=lint /src/go.sum /dev/null ARG VERSION=dev # Install build dependencies for CGO (mattn/go-sqlite3) and sqlite3 CLI (tests) RUN apk add --no-cache make build-base sqlite WORKDIR /src # Copy go mod files first for better layer caching COPY go.mod go.sum ./ RUN go mod download # Copy source code COPY . . # Run tests. See the CHECK_EPOCH comment in the lint stage for the # mechanism; ARG scope is per-stage, so this stage needs its own # declaration, its own guard, and its own expansion, and they must stay # immediately above the check RUN. ARG CHECK_EPOCH RUN [ -n "$CHECK_EPOCH" ] || exit 1 RUN echo "check epoch: ${CHECK_EPOCH}" && make test # Build (pure Go, no CGO required since we use modernc.org/sqlite) RUN CGO_ENABLED=0 go build -ldflags "-X 'sneak.berlin/go/vaultik/internal/globals.Version=${VERSION}' -X 'sneak.berlin/go/vaultik/internal/globals.Commit=$(git rev-parse HEAD 2>/dev/null || echo unknown)' -X 'sneak.berlin/go/vaultik/internal/globals.CommitDate=$(git show -s --format=%cs HEAD 2>/dev/null || echo unknown)'" -o /vaultik ./cmd/vaultik # Runtime stage # alpine:3.21, 2026-02-25 FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709 RUN apk add --no-cache ca-certificates sqlite # Copy binary from builder COPY --from=builder /vaultik /usr/local/bin/vaultik # Create non-root user RUN adduser -D -H -s /sbin/nologin vaultik USER vaultik ENTRYPOINT ["/usr/local/bin/vaultik"]