Fatskills
Practice. Master. Repeat.
Study Guide: TECH **Docker & Kubernetes: Multi-Stage Builds & Minimal Images – Zero-Fluff Study Guide**
Source: https://www.fatskills.com/kubernetes/chapter/tech-docker-kubernetes-multi-stage-builds-minimal-images-zero-fluff-study-guide

TECH **Docker & Kubernetes: Multi-Stage Builds & Minimal Images – Zero-Fluff Study Guide**

By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.

⏱️ ~8 min read

Docker & Kubernetes: Multi-Stage Builds & Minimal Images – Zero-Fluff Study Guide


1. What This Is & Why It Matters

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.

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.

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).


2. Core Concepts & Components


? Multi-Stage Builds

  • Definition: A Dockerfile with multiple FROM statements, where each stage builds on the previous one, but only the final stage is kept in the output image.
  • Production insight: If you don’t use multi-stage builds, your final image will include build tools, compilers, and temporary files that bloat it and increase attack surface.

? Minimal Base Images

  • Definition: Ultra-lightweight images like alpine, distroless, or scratch that contain only the bare essentials to run your app.
  • Production insight: alpine is ~5MB, while ubuntu is ~70MB—but distroless (Google’s minimal image) has no shell, making it more secure (but harder to debug).

? scratch

  • Definition: The smallest possible Docker image (literally empty). Used for statically compiled binaries (Go, Rust).
  • Production insight: If your app is statically linked, scratch is the most secure option—but you can’t debug inside the container (no sh, ls, etc.).

? alpine

  • Definition: A lightweight Linux distro (~5MB) with apk package manager.
  • Production insight: Great for smaller images, but some apps (like Python) may need extra dependencies (libc6-compat).

? distroless (Google)

  • Definition: Minimal images without a shell or package manager (e.g., gcr.io/distroless/base-debian11).
  • Production insight: More secure than alpine (no shell = no attack surface), but harder to debug (no sh, curl, etc.).

? Builder Pattern (Manual Multi-Stage)

  • Definition: Using two separate Dockerfiles (one for building, one for running) and copying artifacts between them.
  • Production insight: Less flexible than multi-stage builds, but useful if you can’t modify the original Dockerfile.

? .dockerignore

  • Definition: A file that excludes files/dirs from the Docker build context (like .gitignore).
  • Production insight: If you don’t use .dockerignore, your build context balloons in size, slowing down builds.

? Layer Caching

  • Definition: Docker caches each layer of a build. If a layer hasn’t changed, it reuses the cache.
  • Production insight: Order matters—put frequently changing layers last (e.g., COPY . . should be near the end).


3. Step-by-Step Hands-On: Multi-Stage Build for a Go App


Prerequisites

✅ Docker installed (docker --version should work).
✅ A Go app (or use this example).

Task: Build a tiny Go binary in a multi-stage Dockerfile and deploy it to Kubernetes.



Step 1: Write a Basic (Bloaty) Dockerfile

# 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.


Step 2: Rewrite as a Multi-Stage Build

# 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"]

Build & check size:


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.


Step 3: Use scratch for Maximum Minimalism

# 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"]

Build & check size:


docker build -t hello-scratch .
docker images | grep hello-scratch # Output: hello-scratch latest 1.8MB

Why this works:
- CGO_ENABLED=0statically links all dependencies (no dynamic libs needed).
- scratchno OS, no shell, just the binary.
- Most secure (no attack surface).


Step 4: Deploy to Kubernetes

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!"


4. ? Production-Ready Best Practices


? Security

  • Use distroless or scratch for production (no shell = no attack surface).
  • Scan images for CVEs (docker scan, trivy, or snyk).
  • Sign images with cosign (prevent tampering).
  • Avoid running as root (use USER 1000 in Dockerfile).

? Cost Optimization

  • Multi-stage builds → Smaller images = faster pulls, less storage.
  • Use alpine or distroless instead of ubuntu/debian.
  • Set imagePullPolicy: IfNotPresent in Kubernetes to avoid unnecessary pulls.

?️ Reliability & Maintainability

  • Pin base image versions (FROM alpine:3.18 not FROM alpine:latest).
  • Use .dockerignore to exclude node_modules, .git, etc.
  • Tag images with Git SHA (your-image:$(git rev-parse --short HEAD)).
  • Test image size in CI (docker images | grep $IMAGE_NAME).

? Observability

  • Add health checks (HEALTHCHECK in Dockerfile or livenessProbe in Kubernetes).
  • Log to stdout/stderr (Kubernetes captures these by default).
  • Monitor image pull times (slow pulls = large images).


5. ⚠️ Common Mistakes & Traps

Mistake Symptom Fix/Prevention
Not using multi-stage builds Final image is 1GB+ and includes gcc, npm, etc. Rewrite Dockerfile with FROM ... AS builder and COPY --from=builder.
Using alpine without libc6-compat Python/Node apps crash with Error: libc not found. Add RUN apk add --no-cache libc6-compat in Alpine.
Forgetting .dockerignore Builds take 10+ minutes and fail with "no space left". Add .dockerignore to exclude node_modules, .git, etc.
Using latest tag for base images Production breaks when alpine:latest updates. Pin versions (FROM alpine:3.18).
Not statically linking Go binaries scratch image fails with no such file or directory. Use CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"'.


6. ? Exam/Certification Focus


Typical Question Patterns

  1. "Which base image is the most secure for a Go app?"
  2. scratch (no shell, no attack surface).
  3. alpine (has a shell, more CVEs).
  4. ubuntu (bloated, many CVEs).

  5. "How do you reduce a Node.js image from 1GB to 100MB?"

  6. ✅ Multi-stage build: FROM node:18 AS builderFROM alpine:latestCOPY --from=builder.
  7. ❌ Just use alpine (still includes node_modules).

  8. "What’s the difference between scratch and distroless?"

  9. scratch = empty (no OS, no shell).
  10. distroless = minimal OS (e.g., base-debian11) but no shell.

  11. "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"]

  12. ❌ Uses ubuntu:latest (bloated, insecure).
  13. ❌ No multi-stage (includes apt cache).
  14. ✅ Fix: Use python:3.11-slim + multi-stage.

Key Trap Distinctions

Concept Trap Why It Matters
Multi-stage vs. single-stage Single-stage keeps build tools in final image. Multi-stage discards them, reducing size & CVEs.
scratch vs. alpine scratch has no shell, alpine does. scratch is more secure but harder to debug.
Layer caching order Putting COPY . . early invalidates cache on every change. Put frequently changing layers last.


7. ? Hands-On Challenge (with Solution)


Challenge:

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.

Task: Rewrite it as a multi-stage build using alpine to reduce size.


Solution:

# 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.

Verify:


docker build -t flask-multi-stage .
docker images | grep flask-multi-stage # Output: flask-multi-stage latest ~100MB (vs. 950MB)


8. ? Rapid-Reference Crib Sheet


Dockerfile Commands

Command Purpose Example
FROM ... AS builder Start a build stage FROM golang:1.21 AS builder
COPY --from=builder Copy from another stage COPY --from=builder /app /app
RUN --mount=type=cache Cache build dependencies RUN --mount=type=cache,target=/root/.cache pip install -r requirements.txt
USER 1000 Run as non-root USER 1000
HEALTHCHECK Define health check HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost/health || exit 1

Base Images

Image Size Use Case ⚠️ Trap
scratch 0MB Statically linked binaries (Go, Rust) No shell = hard to debug
alpine ~5MB General-purpose minimal image Some apps need libc6-compat
distroless ~20MB Secure production apps No shell = no sh or curl
python:3.9-slim ~120MB Python apps Still larger than alpine

Kubernetes Optimizations

Setting Purpose Example
imagePullPolicy: IfNotPresent Avoid unnecessary pulls imagePullPolicy: IfNotPresent
resources.requests/limits Prevent OOM kills resources: { requests: { cpu: "10m", memory: "50Mi" } }
securityContext.runAsNonRoot Run as non-root securityContext: { runAsNonRoot: true }


9. ? Where to Go Next

  1. Docker Multi-Stage Docs – Official guide.
  2. Google Distroless Images – Minimal, secure images.
  3. Trivy Image Scanner – Scan for CVEs in images.
  4. Kubernetes Best Practices: Images – Official recommendations.


ADVERTISEMENT