LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
Lesson 1112 min read

Multi-Stage Builds: Small, Fast, Secure Images

Use multi-stage builds to compile your app with a full toolchain and ship only the result — shrinking a 508 MB image to 8 MB.

On this page 11 sections

Introduction

To build many applications you need compilers, package managers and build tools — but to run them, you need none of that. Multi-stage builds let one Dockerfile use a big image for building and a tiny image for running, so your final image contains only what the application needs.

What You Will Learn
  • Why single-stage images often carry hundreds of megabytes of build tools.
  • How to write a multi-stage Dockerfile with FROM ... AS and COPY --from.
  • What the empty scratch image is, and the trade-offs of images without a shell.
  • How to build a single stage with --target, and how the pattern applies to other languages.

The Problem: Build Tools in Your Image

Priya has written a small web service in Go. Go compiles a program into a single executable file, which makes it a perfect example. Her project has three files:

tree
Output

Click Run to see what this code prints.

main.go
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
host, _ := os.Hostname()
fmt.Fprintf(w, "Hello from a tiny Go container (%s)\n", host)
})
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

Her first Dockerfile is a straightforward single stage: start from the official Go image, copy the code, compile it, run it:

Dockerfile (single stage)
FROM golang:1.27-alpine
WORKDIR /src
COPY . .
RUN go build -o /hello .
EXPOSE 8080
CMD ["/hello"]
docker build -t hello-go:single .
Output

Click Run to see what this code prints.

It works, but the image includes the entire Go compiler and standard library source — over 70 MB just for one of its layers — none of which the finished program needs.

Writing a Multi-Stage Dockerfile

A multi-stage Dockerfile has several FROM lines. Each starts a new stage, and only the last stage becomes the final image. Earlier stages can be named with AS, and later stages copy files out of them with COPY --from:

Dockerfile (multi-stage)
# Stage 1: build the program with the full Go toolchain
FROM golang:1.27-alpine AS build
WORKDIR /src
COPY go.mod ./
COPY *.go ./
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /hello .
# Stage 2: copy only the finished program into an empty image
FROM scratch
COPY --from=build /hello /hello
EXPOSE 8080
ENTRYPOINT ["/hello"]
  • FROM golang:1.27-alpine AS build — the build stage, named build.
  • CGO_ENABLED=0 makes a fully static program that needs no system libraries; -ldflags="-s -w" strips debugging information to make it smaller.
  • FROM scratch — the final stage starts from scratch, a special, completely empty image.
  • COPY --from=build /hello /hello copies just the compiled program from the build stage.
docker build -t hello-go:multi .
Output

Click Run to see what this code prints.

BuildKit labels each step with its stage: [build 1/5] to [build 5/5] for the build stage, then [stage-1 1/1] for the final, unnamed stage. Note that WORKDIR /src was CACHED — it was identical to the single-stage build.

Comparing the Results

docker images hello-go
Output

Click Run to see what this code prints.

From 508 MB to 8.38 MB on disk — about 60 times smaller, and only 2.52 MB to download. It still works exactly the same:

docker run -d --name hello -p 8080:8080 hello-go:multi > /dev/null
sleep 1
curl -s http://localhost:8080/
docker logs hello
Output

Click Run to see what this code prints.

BenefitWhy it matters
Faster downloads and deploysServers pull 2.5 MB instead of 100 MB on every release
Less storageRegistries and servers keep many versions of each image
Smaller attack surfaceNo shell, compiler or package manager for an attacker to use
Fewer vulnerability reportsSecurity scanners have almost nothing to flag
Mini Challenge · Predict the Output

In Priya’s multi-stage Dockerfile, which of these ends up in the final hello-go:multi image?

Choose one answer

Images Without a Shell

The scratch image really is empty — there is not even a shell, so you cannot exec into the container:

docker exec -it hello sh
Output

Click Run to see what this code prints.

That is good for security but makes debugging harder. If you need a few basic tools, use a small base such as alpine for the final stage instead of scratch. Another popular choice is Google’s “distroless” images, which contain language runtimes and CA certificates but no shell or package manager. Note also that scratch has no CA certificates: a Go program that calls HTTPS APIs needs them copied in from the build stage (COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/).

Building One Stage: --target

--target stops the build at a named stage. That is handy for debugging the build itself, or for running tests in the build environment:

docker build -q --target build -t hello-go:build-stage . > /dev/null
docker run --rm hello-go:build-stage go version
Output

Click Run to see what this code prints.

Multi-Stage for Other Languages

LanguageBuild stageFinal stage
Go, RustCompile with the full toolchainscratch, distroless or alpine with just the binary
JavaBuild a .jar with Maven or Gradle (JDK image)A JRE-only image with the .jar
React, Vue, Angularnpm ci && npm run build (Node image)nginx:alpine serving the generated static files
PythonBuild wheels for packages that need compilingpython:slim installing the prepared wheels
.NETdotnet publish (SDK image)The smaller ASP.NET runtime image

A front-end example — the Node image builds the site, and Nginx serves the result:

Dockerfile (front-end example)
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
Mini Challenge · Put It in Order

Put the parts of a multi-stage Dockerfile for a compiled app in order.

  1. FROM a minimal runtime image
  2. COPY --from=build the compiled output
  3. Copy the dependency list and download dependencies
  4. Copy the source code and compile it
  5. Set the command with CMD or ENTRYPOINT
  6. FROM a full SDK image AS build

Common Mistakes

Avoid These Mistakes
  • Copying a dynamically linked program into scratch, where its system libraries do not exist — build it statically (CGO_ENABLED=0 for Go).
  • Forgetting CA certificates or time zone data in a scratch image when the app needs them.
  • Using COPY --from with a stage name that does not match the AS name.
  • Building the front end in the final stage and shipping node_modules to production.

Frequently Asked Questions

No — often it is faster overall, because each stage caches independently, and the final image is quicker to push and pull.

Yes. COPY --from=nginx:alpine /etc/nginx/nginx.conf /tmp/ copies a file straight out of any image.

Use it whenever building needs tools that running does not. For a simple Python app with no compiled packages, a single slim stage is often enough.

Key Takeaways

  • Multi-stage builds separate the build environment from the runtime image.
  • Name stages with FROM ... AS name; copy results with COPY --from=name.
  • Only the last stage becomes the image — Priya’s went from 508 MB to 8.38 MB.
  • scratch is empty: no shell, no certificates. Alpine or distroless are useful middle grounds.
  • --target builds a single stage, useful for debugging and tests.

Summary

You can now build images that are small, fast and safe. Next you will learn to diagnose containers that crash, refuse to start or behave strangely.

Next Lesson →

Debugging Containers