Container Security

Multi-Stage Docker Build Security in 2026

Multi-stage builds are the right way to ship secure container images, but the security benefits depend on getting the stage boundaries right. A guide for 2026.

Hritik Sharma
Staff Engineer
7 min read

Multi-stage Docker builds are a decade old at this point, and the basic pattern, build in one stage with a fat toolchain and copy artifacts into a slim runtime stage, is one of those rare practices that improves both performance and security at the same time. The pattern is also commonly implemented in ways that capture none of the security benefit, because the stage boundaries are drawn in the wrong places or the final stage is not as minimal as the author thinks.

This post is about how to draw the boundaries correctly in 2026, the runtime image choices that have aged well, and the mistakes that defeat the point of the pattern.

Why does the runtime stage choice matter so much?

The runtime stage is the image that actually ships to production, and its surface area is the surface area that attackers and CVE scanners have to work with. A multi-stage build that copies a Go binary into a node:20 runtime stage has done none of the work the pattern was supposed to do, because the runtime stage still includes a full Node.js installation, the npm tooling, the underlying Debian or Alpine base, and the long tail of utilities those bring in. The build stage and the runtime stage are interchangeable in security terms, and the multi-stage pattern is just a more complicated way to ship the same image.

The runtime stage should be the minimum that actually runs the application. For a statically linked Go binary, this is scratch or gcr.io/distroless/static. For a Python application, it is gcr.io/distroless/python3 or one of the slim variants. For Java, distroless Java images or one of the JRE-only variants. The Chainguard images and the Wolfi-based ecosystem have become the strongest contender in 2026 for organizations that want a maintained source of zero-CVE minimal images, because they are rebuilt frequently against fresh source and they carry signed attestations by default.

What are the most common boundary mistakes?

The most common mistake is leaving the package manager in the final stage. A Dockerfile that ends with FROM alpine:3.20 and then RUN apk add --no-cache ca-certificates has apk in the resulting image, which means an attacker with code execution in the container can install arbitrary additional tooling at runtime. The fix is to do the package install in an earlier stage and copy the resulting files into a stage that does not have a package manager. Distroless images are the canonical example of this principle; they ship with the runtime and the application's dependencies, and nothing else.

The second most common mistake is leaving the source code in the final stage. A Dockerfile that does COPY . . in an early stage, builds, and then copies the build output to the runtime stage but accidentally also copies the source directory has shipped the source code to production. This is a confidentiality issue if the source is proprietary, but it is also a supply chain issue, because the source directory often contains development-only dependencies, CI configuration, and other artifacts that should not be in a runtime image. Be explicit about what you copy out of build stages. COPY --from=builder /app/dist /app is correct; COPY --from=builder / / is a mistake.

How should you handle build-time and runtime CVEs separately?

A useful mental model is that the build stage and the runtime stage have different threat models, and they deserve different CVE scanning treatment. The runtime stage is what attackers see; CVEs there are first-class risks that map directly to production exposure. The build stage is internal infrastructure; CVEs there matter primarily as a path to a build-time compromise, where a malicious dependency could inject code into the resulting artifact.

This means your scanning pipeline should report runtime stage CVEs as production findings and build stage CVEs as build infrastructure findings, with different prioritization. A CVE in the npm version used in the build stage to compile a Rust binary that ends up in the runtime stage is a low-priority finding; the same CVE in a Node.js application's runtime stage is a high-priority finding. Most scanners do not make this distinction by default and report everything against a single severity scale, which is the right place to apply reachability analysis. A CVE in code that never ends up in the runtime stage is not reachable in any meaningful sense.

What about the COPY operations themselves?

The COPY --from= instructions are the bridge between stages, and they are also the place where supply chain mistakes propagate from build to runtime. A common pattern is to copy the entire node_modules directory from a build stage into the runtime stage, which carries every development dependency, every test framework, and every transitive dependency of those, into production. The fix is to run a production-only install in a dedicated stage, with npm ci --omit=dev or the equivalent, and copy from that stage rather than from a stage that included dev dependencies.

The same principle applies to Python with pip install --no-deps patterns combined with a constraints file, to Go with the standard pattern of copying only the compiled binary, and to Java with jdeps-based JRE construction. The 2024 incidents involving NPM packages with malicious postinstall scripts that activated only in production environments illustrated why this matters; the malicious code shipped to production because the entire node_modules tree was copied from a build stage where the install ran without segregation.

What does a 2026-grade multi-stage Dockerfile look like?

The 2026 pattern, for a typical Node.js service, is roughly four stages. A base stage that pins a digest-referenced minimal Node.js image and installs only the package manager tooling. A dependencies stage that runs npm ci in a deterministic way, with BuildKit cache mounts and a secret mount for any private registry credentials. A build stage that compiles or bundles the application using the dependencies. A runtime stage based on a distroless or Wolfi-based Node.js runtime that copies only the bundled output and the production-only dependencies from the dependencies stage.

Each stage is pinned by digest, the build runs with --sbom=true --provenance=true to generate native attestations, and the final image is signed with Cosign or sigstore. The result is an image that is small, has minimal attack surface, has full supply chain metadata attached, and is reproducible from the same source. None of this is exotic; it is the result of taking the established patterns and applying them consistently.

How Safeguard Helps

Safeguard analyzes multi-stage builds by stage, not as a single flattened image, which means the scan output distinguishes build infrastructure CVEs from runtime CVEs in production. Reachability analysis runs against the runtime stage's actual exposure, filtering out the noise from build-only dependencies. Griffin AI evaluates each base image, each frontend, and each pinned digest against our TPRM scoring and zero-day feed, surfacing the components that need attention. Our zero-CVE image catalog gives teams a maintained set of vetted runtime bases that meet the standard out of the box. Policy gates in CI block image promotion when the build introduces a stage regression, like a package manager appearing in the runtime layer or a dev dependency leaking past a stage boundary. The pattern works when it is enforced; the platform is the enforcement layer.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.