How to Reduce Docker Image Size With Multi-Stage Builds
A multi-stage Dockerfile shrinks bloated images by keeping build-time intermediates - compiled code, asset bundles, downloaded deps - in a disposable builder stage. Only the files the runtime actually needs end up in the final image.
Why single-stage Dockerfiles get bloated
A single-stage build accumulates everything in one image: the base OS, build
tools, intermediate artifacts, and the finished app. Every RUN layer that
creates a large file adds to the final image size - even if you delete the file
in a later layer, Docker keeps the data in the earlier layer's snapshot.
A 200MB preprocessing artifact generated at build time stays in the image forever in a single-stage build. On a team doing many deploys a day, that translates to minutes of pull time on every rollout.
The multi-stage pattern
Introduce a named builder stage with AS builder. The second FROM starts a
fresh image - it inherits no layers from the builder. Use COPY --from=builder
to pull only the outputs you actually need.
# Stage 1 - builder: compile, bundle, generate assets, install build deps
FROM python:3.12-slim AS builder
WORKDIR /build
RUN dd if=/dev/urandom of=/build-artifact.bin bs=1M count=200 2>/dev/null \
&& sha256sum /build-artifact.bin > /build/manifest.txt
# Stage 2 - runtime: starts fresh, pulls only what it needs from stage 1
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /build/manifest.txt ./manifest.txt
COPY app.py .
CMD ["python", "app.py"]
The 200MB /build-artifact.bin lives only in the builder layer. The runtime
image contains python:3.12-slim plus manifest.txt and app.py - nothing
else. Image drops from ~370MB to ~150MB without touching the app code.
Measure before and after
# Build and check size
docker build -t myapp .
docker images myapp
# Inspect layers to see where bytes come from
docker history myapp
docker images shows the compressed repository size. docker history breaks
it down layer by layer - useful for spotting which RUN step is the biggest
offender before you start optimizing.
Common real-world variants
- Go binaries - build in
golang:1.22, copy the static binary intoscratchorgcr.io/distroless/static. Final image is often under 10MB. - Node.js -
npm ci+ build innode:20, copydist/andnode_modules/(prod-only, after a secondnpm ci --omit=dev) intonode:20-slim. - Python wheels - install deps in a full
python:3.12layer, copy the virtual environment intopython:3.12-slim.
Why image size matters beyond deploy speed
- Cold-start time - Kubernetes scale-up events, Lambda container images, and Cloud Run all pull from the registry before the first request. A 150MB image starts in seconds; a 1GB image can take minutes.
- Attack surface - fewer files mean fewer CVEs. Distroless images with no shell eliminate whole classes of container-escape vectors.
- Registry storage costs - smaller images cost less to store and transfer, especially across regions.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Writing a two-stage Dockerfile with FROM ... AS builder and a clean runtime stage
- Using COPY --from=builder to pull only required outputs into the final image
- Measuring image size before and after with docker images and docker history
FAQ
How does a multi-stage Docker build reduce image size?
Each FROM starts a fresh filesystem. Build-time intermediates - compilers, asset bundles, downloaded artifacts - stay in the builder stage and are never copied to the runtime stage unless you explicitly use COPY --from=builder. The final image only contains what the runtime stage holds.
What is COPY --from in a Dockerfile?
COPY --from=<stage> copies files from a named build stage into the current stage. You use it to pull just the compiled binary, built assets, or manifest you need out of the builder, leaving everything else behind.
How do I check my Docker image size after optimizing?
Run docker images <tag> to see the compressed size, and docker history <tag> to break it down layer by layer so you can find which step contributes the most bytes.
Why is my Docker image so large?
Usually build tools, caches, and dev dependencies baked into the final image, plus a heavy base. Use a multi-stage build so only runtime artifacts ship, pick a slim or alpine base, and clean package caches in the same layer.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →