Safeguard
Security Guides•

Multi-stage builds: how they shrink a container's attack surface

A multi-stage Dockerfile builds in one stage and ships only the result from another. Here is what that removes, what it does not, and a working example.

Safeguard Security Team
4 min read

A multi-stage build uses several FROM lines in one Dockerfile. You compile or install in an early stage, then copy only the finished artifacts into a clean final stage. Compilers, package managers, source code and build caches stay behind, so the shipped image has fewer files to exploit and fewer packages to patch.

What the final image no longer carries

Without stages, a typical Dockerfile installs a toolchain, builds the app, and ships all of it. Every tool in that image is something an attacker can use after a break-in, and every package is something a scanner will report on.

The Docker documentation describes the effect as copying only the artifacts you need into later stages, "leaving behind everything you don't want in the final image." The build guidance says to split the Dockerfile into distinct stages so the output has only what is needed.

That helps security in three concrete ways.

  • Fewer tools for an intruder. No compiler, no git, no curl, and often no shell.
  • Fewer findings. Packages that are not present cannot show up in a scan or need a patch.
  • No build credentials in the final layers. Tokens used only in an earlier stage do not travel with the image, as long as you also keep them out of the final stage.

A working example

# syntax=docker/dockerfile:1
FROM golang:1.26 AS build
WORKDIR /src
COPY . .
RUN go build -o /bin/app .

FROM scratch
COPY --from=build /bin/app /bin/app
CMD ["/bin/app"]

This is the pattern from the Docker multi-stage documentation. The first stage has the full Go toolchain. The last stage starts from an empty image and receives one binary through COPY with the from option. The result contains the program and nothing else.

Not every language can do this. A Go or Rust binary linked statically can run on an empty image. Python, Node and Java apps need a runtime, so the final stage should be a small base that provides it, not an empty one.

Stopping at a stage

You can build up to a named stage with the target option. That lets you keep a test or debug stage in the same file without shipping it. With BuildKit, only the stages that the target depends on are processed. The caution is simple: whatever stage you tag and push is what people can pull, so make sure the pushed image is the final one.

What multi-stage does not fix

  • The base image still matters. A multi-stage build onto a large base keeps the large base's packages.
  • Anything copied is still shipped. If you copy the whole source folder into the final stage, you are back where you started. Copy specific paths.
  • Secrets copied across stages persist. If a stage writes a credential to a file and a later stage copies it, it is now in a layer. Use build secrets instead. See the build secrets guide.
  • Dependencies still need tracking. The libraries compiled into your binary are still your responsibility.

A practical routine

  1. Write the build stage with whatever tools it needs.
  2. Choose the smallest final stage that can run the app.
  3. Copy named files, not directories you have not reviewed.
  4. Add a USER instruction in the final stage.
  5. Scan the final image and read the file list once.

Safeguard's SBOM and dependency scanning can list what ended up in the shipped image, including libraries compiled into it, and you can try a free scan.

Quick check

Does a multi-stage build make the build itself safer?

Not by itself. It changes what ships. The build stage still runs code and downloads dependencies, so pin inputs and keep credentials out of it.

Can I use multi-stage builds with any language?

Yes for the pattern, but the final stage differs. Static binaries can use an empty image. Interpreted languages need a base that includes the runtime.

Sources

Last reviewed: 19 October 2026.

Never miss an update

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

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.