CI Perf Lint

dockerfile-runtime-copies-builder-rootfs

Why it matters

COPY --from=build / / copies the builder’s merged filesystem, including any compiler, source and build-only files still present. If the output image only needs the compiled application and runtime assets, this creates avoidable snapshot, export, push, pull and extraction work.

This is different from FROM build: COPY does not inherit the source stage’s historical layers or its image configuration. Files already deleted from the source filesystem are not copied.

What it flags

A literal full-root COPY from a local builder into the root of the selected output stage or its rootfs ancestry, when:

Named and numeric COPY selectors, exec-form instructions and literal action/CLI build targets are supported. Literal ENV paths, known official-image defaults such as Go’s GOPATH=/go, and named-stage inheritance resolve WORKDIR. A literal cp can propagate a compiled output into the path executed by the runtime.

Root source paths are normalized, so /, /. and repeated root separators are equivalent. The destination is resolved using the WORKDIR and literal environment visible at the COPY instruction, not a later WORKDIR. Relative destinations that resolve to / are included; subdirectory destinations and unresolved paths are excluded. Multiple-source and wildcard COPY instructions do not establish a literal full-root transfer.

Default Go output inference requires a root go.mod, a root non-test main package, and a visible COPY supplying that module to the build WORKDIR. Only simple root-package builds are inferred. Unknown GOFLAGS/workspace/config overrides, version-suffixed module paths, library-only packages, arbitrary package targets, custom compiler bases, wrappers and opaque filesystem changes are excluded.

Unknown build targets, dynamic bases/selectors, external source images, PATH-only commands, tooling/dev/test output names or Dockerfile paths, and ambiguous relative paths are excluded. Compiler removal suppresses the finding rather than treating a cleaned rootfs as a compiler-heavy output.

The rule does not flag copying a compiler stage’s selected artifacts, or transferring a runtime-only rootfs assembled in scratch. FROM inheritance is covered by the existing build/runtime separation diagnostic. Custom compiler bases, Cargo wrappers and arbitrary build scripts are outside the initial scope. One finding per Dockerfile; no automatic fix.

Suggested action

FROM golang:1.26 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app

FROM scratch
COPY --from=build /out/app /out/app
ENTRYPOINT ["/out/app"]

Use scratch only for a verified compatible executable. Copy required shared libraries, CA certificates, timezone data, SQL migrations, configuration and other assets into an appropriate runtime base. Preserve WORKDIR, ownership, permissions and application behavior. Keep whole-root transfers when building a distro, chroot, toolchain or intentionally complete runtime rootfs. A flattening comment alone does not establish that a retained compiler is needed at runtime.

Verification

Compare output image bytes and export, push, cold pull and extraction duration. Verify startup, dynamic linking, runtime assets and behavior. The scanner identifies a reviewable pattern; it does not prove every file outside the executable is unnecessary.

References