Building Docker Images
Multi-Stage Builds — Smaller, Faster, Safer Images
Compiling binaries in build containers and copying outputs to runtime images.
Interview: A cornerstone SRE concept. Interviewers will test your ability to refactor single-stage Dockerfiles to reduce storage footprints and minimize host security vulnerabilities.
Introduction
Compiling source code requires heavy build tools: compilers (like gcc or g++), build managers (like maven or npm), development headers, and dependency caches. However, your application only needs a runtime environment (like a JRE, a Node engine, or just a raw system execution path) to run.
Multi-Stage Builds solve this by allowing you to define multiple temporary build stages in a single Dockerfile, and copy only the compiled execution binaries into the final lightweight production image.
Why It Matters
Single-stage builds bundle all development tools into the final production image. This causes:
• **Huge Image Sizes:** A Go API image can easily exceed 1GB.
• **Vulnerability Exposure:** Extra packages (like compilers, package managers, and debug tools) inflate the container's attack surface and trigger high CVE alerts.
How It Works
In a multi-stage Dockerfile, you declare multiple FROM instructions. Each FROM starts a new build stage. You can name stages using AS name:
In this flow, the heavy golang:1.21 image is discarded after compilation. The final image is based on the 5MB alpine image, containing only the static binary.
Practical Example
Let's look at a React frontend application. Compiling React requires Node.js, but serving the final static HTML/CSS/JS code only requires Nginx:
Quick Quiz
Q1: What happens to the intermediate stages (e.g. "compiler" stage) after a Docker build completes?
A) They are uploaded to the remote image registry.
B) They remain inside the final container filesystem, hidden from view.
C) They are discarded, and only the final stage layers form the output image.
D) The build fails if they are not cleaned manually.
Answer: C — The intermediate layers are bypassed. Only the layers defined in the final FROM block are packaged into the output image tag.
Scenario-Based Challenge
Production Scenario:
A legacy Java application builds its fat JAR inside a single-stage Dockerfile based on maven:3.8-openjdk-11. The final image size is 1.4GB. Vulnerability scans show 450 vulnerabilities (including package manager shell injection bugs). How do you refactor this Dockerfile to secure it and reduce size?
Refactor the Dockerfile using a multi-stage approach to separate Maven compilation from execution:
This refactoring drops Maven compilers and source files, shrinking the image size to ~250MB and reducing CVE vulnerabilities by over 80%.
Interview Questions
1. Can you copy files from external images instead of intermediate stages?
Yes. The COPY --from directive accepts external registry images. For example, COPY --from=nginx:latest /etc/nginx/nginx.conf /my/path/ allows you to pull files directly from existing public images without compiling them.
2. How do you stop a multi-stage build at a specific stage during local testing?
You can specify the target stage using the --target flag: docker build --target builder -t dev-app .. This compiles the image up to the specified stage and exits, which is highly useful for running tests in intermediate compile environments.
Production Considerations
Use named builder stages for readability. While you can reference stages by index (e.g., COPY --from=0 /app .), this is fragile. If you insert a new stage at the top of the Dockerfile, all index offsets shift, corrupting the copy instructions.