What a multi-stage build is
A multi-stage build is a Dockerfile with several FROM sections: each one starts a new, independent stage with its own base image. Only the last stage —usually tagged runtime— becomes the final image; every earlier stage exists only during docker build and disappears afterwards. COPY --from=build copies exclusively what you need (the compiled binary) from one stage into another, without dragging the compiler, development headers or the rest of the toolchain into production.
Why image size matters
A smaller production image pulls and deploys faster, but more importantly it shrinks the attack surface: fewer installed packages means fewer potential CVEs and fewer tools (compilers, full shells, package managers) an attacker could lean on after compromising the container. That’s why this lab compares the real size of a single-stage image against the same app built multi-stage with docker images, and confirms the final stage doesn’t even have the Go compiler installed.
.dockerignore and –target
The .dockerignore file excludes files from the build context, the same way .gitignore excludes files from a commit: local notes, logs or .git itself never reach docker build, so they can’t leak into the image either. The docker build --target <stage> flag builds just one named stage, handy for debugging the build stage on its own without also having to complete the runtime stage.
When to reach for Alpine, scratch or distroless
Alpine (built on musl and BusyBox) is the usual starting point: it weighs around 5 MB and ships a minimal shell for debugging. If the binary is fully static (like this lab’s, built with CGO_ENABLED=0), FROM scratch shrinks the image even further by including no base system at all. Google’s distroless images sit in between: no shell, no package manager, but the OS libraries that many binaries do need.
If you go on to publish these images, our lab on setting up a private Docker image registry is a natural next step, along with scanning them with Trivy and Grype before you push, managing them from Portainer on Docker Compose, and, if all of this runs on your own hardware, the rest of our self-hosted home lab series. The official reference lives in Docker’s multi-stage build docs, and for picking the lightest base image see the official Alpine Linux images or OWASP’s Docker Security Cheat Sheet.
FAQ
What is a multi-stage build in Docker?
It's a Dockerfile with several FROM sections, each one a separate stage. Only the last stage becomes the final image; earlier stages exist only during the build and are used to compile or prepare dependencies without that weight reaching production.
Why use Alpine for the final stage instead of the full image?
Because Alpine weighs around 5 MB versus the hundreds of MB (or over 1 GB) of a full development image with a compiler and libraries. Fewer installed packages also means a smaller attack surface and fewer CVEs to track.
How do I build just the build stage with –target?
Run docker build –target build -t my-image:build-stage . Docker stops right after finishing that stage, the one named build in the Dockerfile, without running the rest; handy for debugging it or inspecting its artifacts on their own.
Does a multi-stage build make the image more secure, or just smaller?
Both, and they're related: by leaving the compiler, package managers and development tools out of the final image, there's less installed software an attacker could exploit if they compromise the container. That's why this lab checks with command -v go that the runtime image doesn't even have the Go compiler.