By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
You’re deploying a microservice to Kubernetes, and your container image is 1.2GB. Your CI/CD pipeline takes 10 minutes to build and push it. Your cluster nodes are running out of disk space, and your security team flags 12 critical CVEs in the base image. Meanwhile, your cloud bill is climbing because every pod is pulling a bloated image.
Multi-stage builds let you slim down your final image by discarding build-time dependencies (compilers, SDKs, temp files) while keeping only what your app needs to run. Minimal images (like alpine, distroless, or scratch) reduce attack surface, speed up deployments, and cut storage costs.
alpine
distroless
scratch
Real-world scenario:- You inherit a legacy Node.js app with a Dockerfile that installs g++, python, and make just to run npm install. The final image is 900MB and includes unnecessary tools that attackers can exploit.- Your Kubernetes cluster is failing to schedule pods because nodes are full, and your CI/CD pipeline is timing out during image pulls.
g++
python
make
npm install
If you ignore this:❌ Slower deployments (larger images = longer pull times).❌ Higher cloud costs (storage + bandwidth).❌ Security risks (more CVEs in bloated images).❌ Cluster instability (nodes run out of disk space).
If you master this:✅ Faster CI/CD (smaller images build/push/pull faster).✅ Lower costs (less storage, bandwidth, and compute waste).✅ Hardened security (fewer attack vectors).✅ More reliable clusters (nodes stay healthy longer).
FROM
ubuntu
sh
ls
apk
libc6-compat
gcr.io/distroless/base-debian11
curl
.dockerignore
.gitignore
COPY . .
✅ Docker installed (docker --version should work).✅ A Go app (or use this example).
docker --version
# BAD: Single-stage, bloated image FROM golang:1.21 WORKDIR /app COPY . . RUN go mod download RUN go build -o /hello CMD ["/hello"]
Build & check size:
docker build -t hello-bloated .docker images | grep hello-bloated # Output: hello-bloated latest 1.1GB
Problem: The final image includes Go compiler, git, curl, etc.—all unnecessary for running the binary.
git
# GOOD: Multi-stage build # Stage 1: Build the binary FROM golang:1.21 AS builder WORKDIR /app COPY . . RUN go mod download RUN go build -o /hello # Stage 2: Copy only the binary to a minimal image FROM alpine:latest WORKDIR /app COPY --from=builder /hello /hello CMD ["/hello"]
docker build -t hello-multi-stage .docker images | grep hello-multi-stage # Output: hello-multi-stage latest 7.3MB
Result: 99% smaller (1.1GB → 7.3MB) because we discarded the Go compiler in the final image.
# BEST: Multi-stage + scratch FROM golang:1.21 AS builder WORKDIR /app COPY . . RUN go mod download RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o /hello FROM scratch COPY --from=builder /hello /hello CMD ["/hello"]
docker build -t hello-scratch .docker images | grep hello-scratch # Output: hello-scratch latest 1.8MB
Why this works:- CGO_ENABLED=0 → statically links all dependencies (no dynamic libs needed).- scratch → no OS, no shell, just the binary.- Most secure (no attack surface).
CGO_ENABLED=0
1. Push the image to a registry:
docker tag hello-scratch your-registry/hello-scratch:v1 docker push your-registry/hello-scratch:v1
2. Deploy to Kubernetes:
# hello-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: hello spec: replicas: 2 selector: matchLabels: app: hello template: metadata: labels: app: hello spec: containers: - name: hello image: your-registry/hello-scratch:v1 ports: - containerPort: 8080 resources: requests: cpu: "10m" memory: "10Mi" limits: cpu: "50m" memory: "50Mi"
Apply the deployment:
kubectl apply -f hello-deployment.yaml
Verify:
kubectl get pods -o wide kubectl logs <pod-name> # Should output: "Hello, world!"
docker scan
trivy
snyk
cosign
root
USER 1000
debian
imagePullPolicy: IfNotPresent
FROM alpine:3.18
FROM alpine:latest
node_modules
.git
your-image:$(git rev-parse --short HEAD)
docker images | grep $IMAGE_NAME
HEALTHCHECK
livenessProbe
stdout
stderr
gcc
npm
FROM ... AS builder
COPY --from=builder
Error: libc not found
RUN apk add --no-cache libc6-compat
latest
alpine:latest
no such file or directory
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"'
❌ ubuntu (bloated, many CVEs).
"How do you reduce a Node.js image from 1GB to 100MB?"
FROM node:18 AS builder
❌ Just use alpine (still includes node_modules).
"What’s the difference between scratch and distroless?"
distroless = minimal OS (e.g., base-debian11) but no shell.
base-debian11
"Why does this Dockerfile fail in production?" dockerfile FROM ubuntu:latest RUN apt-get update && apt-get install -y python3 COPY . . CMD ["python3", "app.py"]
dockerfile FROM ubuntu:latest RUN apt-get update && apt-get install -y python3 COPY . . CMD ["python3", "app.py"]
ubuntu:latest
apt
python:3.11-slim
You have a Python Flask app with this Dockerfile:
FROM python:3.9 WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD ["python", "app.py"]
Problem: The final image is 950MB and includes gcc, pip cache, etc.
pip cache
Task: Rewrite it as a multi-stage build using alpine to reduce size.
# Stage 1: Build dependencies FROM python:3.9 AS builder WORKDIR /app COPY requirements.txt . RUN pip install --user -r requirements.txt # Stage 2: Copy only what's needed FROM python:3.9-alpine WORKDIR /app COPY --from=builder /root/.local /root/.local COPY . . # Ensure scripts in .local are usable ENV PATH=/root/.local/bin:$PATH CMD ["python", "app.py"]
Why it works:- Stage 1 installs dependencies (keeps pip cache out of final image).- Stage 2 uses alpine (~50MB vs. 950MB) and copies only the installed packages.- --user flag avoids root permissions.
--user
docker build -t flask-multi-stage .docker images | grep flask-multi-stage # Output: flask-multi-stage latest ~100MB (vs. 950MB)
FROM golang:1.21 AS builder
COPY --from=builder /app /app
RUN --mount=type=cache
RUN --mount=type=cache,target=/root/.cache pip install -r requirements.txt
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/health || exit 1
python:3.9-slim
resources.requests/limits
resources: { requests: { cpu: "10m", memory: "50Mi" } }
securityContext.runAsNonRoot
securityContext: { runAsNonRoot: true }
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.