Docker Multi-Stage Builds: Smaller and More Secure Images

· 2 min read · Docker & Rancher Desktop Tutorials

The problem: build tools in production #

To compile an application you need a compiler, headers, a package manager and test tools. To run it you only need the result. If you build and run in the same image, you ship hundreds of megabytes that you never use, plus a shell and tools an attacker would love to find.

A multi-stage build uses several FROM instructions in one Dockerfile. Each FROM starts a new stage. You build in the first stage and copy only the artifact into the final, small stage. Everything else is discarded.

Multi-stage build: a first stage with the compiler and sources builds the binary, and the final stage copies only the binary into a small runtime image
Only the last stage becomes the image you push

Example in Go #

main.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello from a tiny container")
	})
	http.ListenAndServe(":8080", nil)
}
Dockerfile
# ---- Stage 1: build
FROM golang:1.24 AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
# A static binary does not need libc in the final image
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server .

# ---- Stage 2: runtime
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

Create the module file and build:

$ go mod init example.com/hello    # or create go.mod by hand
$ docker build -t hello-go:1.0 .
$ docker image ls hello-go
REPOSITORY   TAG   IMAGE ID       CREATED          SIZE
hello-go     1.0   9a1c0f5e1b2d   10 seconds ago   7.4MB
$ docker run --rm -p 8080:8080 hello-go:1.0

Compare it with golang:1.24, which is several hundred megabytes. The final image contains the binary and almost nothing else: no shell, no package manager, and it runs as the non-root user that the :nonroot tag defines.

Example in Node.js #

Interpreted languages still benefit: install the development dependencies and compile in one stage, ship only production dependencies in the other.

Dockerfile
FROM node:22-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM deps AS build
COPY . .
RUN npm run build

FROM node:22-slim AS prod-deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:22-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]

Docker builds only the stages that the final one depends on, and BuildKit builds independent stages (build and prod-deps here) in parallel.

Build one stage: --target #

Name your stages with AS and stop at any of them. This is useful to run tests in CI or to have a debug image:

$ docker build --target build -t myapp:build .
$ docker build --target runtime -t myapp:1.0 .

A common pattern is a test stage that runs the tests, so that docker build --target test . fails the pipeline when a test fails, while the normal build skips it.

Copy from another image #

COPY --from accepts an image as well as a stage. It is a handy way to take a single binary from a published image:

COPY --from=busybox:1.37 /bin/busybox /usr/local/bin/busybox

Choosing the final image #

Final image Size Shell When to use it
scratch 0 No A fully static binary (Go, Rust). You must add CA certificates and users yourself.
distroless Very small No Static or language-runtime images (Java, Python, Node) with a minimal attack surface.
alpine About 5 MB Yes (BusyBox) Small and with a package manager. It uses musl instead of glibc, which can break some binaries.
-slim (Debian) Tens of MB Yes A good default when you need glibc and compatibility.

Distroless images have no shell, so you cannot docker exec ... sh. For debugging, use a debug tag (:debug) or attach a temporary container with docker run --pid container:<name> --network container:<name> -it nicolaka/netshoot.

Next steps #

#Docker #Dockerfile #Security