TOPIC #202Beginner 9 min read

Docker Fundamentals: Images, Layers, & Multi-Stage Builds

CSD
CompleteSystemDesign Editorial
Report an issue
Key takeawayCore Architecture Summary

Package portable cloud-native applications: OCI image specifications, UnionFS layer caching, Dockerfile optimization, non-root security practices, and Multi-Stage builds.

Key Glossary Concepts in this TopicAll Glossary Terms
Interactive Lab · 🐳 Docker Layer Cache LabFull lab guide

Docker Layer Cache & Multi-Stage Build Lab

One cache miss invalidates every subsequent layer. Reorder instructions, split stages, and price the build in CI minutes and CVEs.

Image layer stack — simulating: a source edit

FROM node:20-alpineCACHED ♻ 0s
COPY package*.jsonCACHED ♻ 0s
RUN npm ci --omit=devCACHED ♻ 0s
COPY src/ ./src/REBUILD ↻ 2s
USER node · CMDREBUILD ↻ 0s

Source-edit / dep build

2s / 98s

Monthly CI minutes

750

Final image / pull

190 MB · 4.8s

Open CVEs · shell

41 · /bin/sh yes

Manifest-first ordering keeps npm ci cached across source edits: only the 2s COPY src layer (plus cascade) rebuilds. --omit=dev drops dev tooling, and USER node runs the process unprivileged.

Docker Multi-Stage Build Optimization 📦

Compiling within a heavy development image and copying strictly the static binary to a minimal, non-root production container.

Docker Multi-Stage Build Optimization 📦
100%
Touchpad: Pinch to zoom • Drag to pan
Rendering visual architecture flowchart...

01.OCI Container Image Architecture & UnionFS Layers

An OCI (Open Container Initiative) container image is not a monolithic disk archive. It is an ordered stack of immutable, cryptographically hashed (SHA-256) filesystem diff layers composed via UnionFS (Overlay2):

  • Immutable Read-Only Layers: Every instruction in a Dockerfile (FROM, COPY, RUN) generates a distinct read-only layer containing only the delta (new files, modified files, or whiteout markers for deleted files).
  • Ephemeral Writable Layer (Container Layer): When a container boots, the storage driver mounts all image layers as a unified virtual filesystem (merged) and adds a thin writable layer at the top (upperdir). Any runtime write triggers a Copy-on-Write (CoW) operation.
  • Layer Hash Addressability & Deduplication: If five distinct microservices share the same base layer (FROM node:20-slim), the host server downloads and stores that base layer exactly once, saving gigabytes of disk space and network bandwidth.

02.Dockerfile Optimization: Layer Caching & Instruction Ordering

Docker evaluates build steps sequentially and uses cached layers whenever the instruction and the input files have not changed. A single cache miss invalidates the cache for all subsequent instructions:

Anti-Pattern (Slow Build — Inefficient Cache Use):

dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .                  # ❌ Any 1-character code change invalidates cache here!
RUN npm ci                # ❌ Forces npm to re-download 500MB of packages every build
CMD ["node", "server.js"]

Optimized Production Pattern (Fast Build — Smart Layer Caching):

dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./     # ✅ Only invalidates if dependencies change
RUN npm ci --only=production # ✅ Reused from cache on normal source code edits
COPY src/ ./src/          # ✅ Source code copied AFTER dependency caching
USER node
CMD ["node", "src/server.js"]

Essential Dockerfile Rules:

  1. Order from Least to Most Frequently Changed: Place package manifests (package.json, go.mod, requirements.txt, pom.xml) and dependency installations before copying application source code.
  2. Chain RUN Commands to Minimize Layers: Combine shell operations (apt-get update && apt-get install -y --no-install-recommends pkg && rm -rf /var/lib/apt/lists/*) into a single step so package caches are cleared before the layer commits.
  3. Always Include a .dockerignore File: Exclude .git, node_modules, .env, dist, and temporary IDE files from the build context to accelerate transfer speeds to the Docker daemon.

03.Multi-Stage Builds: Shrinking Images from 1GB to 20MB

In traditional Docker builds, production containers retain development tools, C compilers, package managers (apt, apk), build headers, and devDependencies, inflating image sizes and multiplying security vulnerabilities (CVEs).

Multi-Stage Builds solve this by creating distinct build environments and copying only the compiled artifacts into a lightweight runtime image:

dockerfile
# ------------------------------------------------------------------------------
# STAGE 1: Compilation Environment
# ------------------------------------------------------------------------------
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /bin/app ./cmd/server

# ------------------------------------------------------------------------------
# STAGE 2: Minimal Production Runtime
# ------------------------------------------------------------------------------
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=builder /bin/app /app/server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/server"]

Comparison of Base Images:

  • Ubuntu / Debian Base: ~80 - 150 MB. Contains full package managers, curl, bash, glibc. High CVE surface.
  • Alpine Linux: ~5 MB. Ultra-lightweight, uses musl libc (can introduce memory fragmentation or compatibility bugs with certain C/C++ Python wheels or Node modules).
  • Google Distroless: ~20 MB. Contains strictly your runtime and standard libraries. No shell (/bin/sh), no package manager, preventing attackers from running interactive terminal commands.
  • scratch: 0 MB. Empty image, ideal for completely statically compiled Go or Rust binaries.

04.Production Container Hardening: Non-Root & Capabilities

By default, processes inside a container execute as the root user (UID 0). If an attacker finds a remote code execution (RCE) vulnerability or triggers a container breakout, they inherit root privileges on the underlying host.

Security Hardening Checklist:

  1. Never Run as Root: Create a dedicated unprivileged user (USER 10001:10001) or use distroless :nonroot.
  2. Drop Linux Capabilities: Strip all default root capabilities and selectively grant only what is required:
    yaml
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
        add: ["NET_BIND_SERVICE"]
  3. Mount Root Filesystem as Read-Only: Prevent malware or unauthorized modifications from writing to disk. Provide an in-memory tmpfs mount for ephemeral scratch space at /tmp.

Architectural Trade-offs & Production Realities

Architectural Advantages

  • Multi-stage builds reduce image pull times by up to 90% during Kubernetes horizontal scaling events
  • Distroless base images eliminate package managers and shells, drastically reducing vulnerability CVE count
  • Layer caching drastically speeds up CI/CD pipeline execution from minutes to seconds

Trade-offs & Constraints

  • Multi-stage Dockerfiles require careful pipeline authoring and build target understanding
  • Distroless and scratch images make live production debugging via `docker exec /bin/sh` impossible (requiring ephemeral debug containers)
Production Implementation in Big Tech
Google Distroless• Zero-Vulnerability Base Images

Google created the open-source Distroless container images project. Distroless images contain only the compiled application and minimal runtime libraries (such as glibc or Python/Node runtimes) without package managers, shells, or standard Unix core utilities. This shrinks container sizes to under 30MB and prevents attackers from executing reverse shells or installing unauthorized packages.

Staff+ Engineering Takeaways

  • Docker images are stacks of immutable read-only layers merged via OverlayFS.
  • Structure Dockerfiles with infrequently changing dependencies before frequently changing application code.
  • Use Multi-Stage builds to separate heavy compilers from lean production runtime containers.
  • Harden containers by running as non-root, dropping Linux capabilities, and using read-only root filesystems.

Topic Knowledge Check

Exercise 1 of 3 • Test your architectural comprehension.

Exercise 1 of 30 answered
1

Why should package manifest files (such as package.json or go.mod) be copied and installed BEFORE copying the rest of the application source code in a Dockerfile?

Rate This Architecture ChapterFeedback & Rating

How clear and actionable was this distributed systems breakdown?

Interactive Engineering Workbenches: