Docker Fundamentals: Images, Layers, & Multi-Stage Builds
Package portable cloud-native applications: OCI image specifications, UnionFS layer caching, Dockerfile optimization, non-root security practices, and Multi-Stage builds.
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
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.
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):
dockerfileFROM 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):
dockerfileFROM 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:
- 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. - 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. - Always Include a
.dockerignoreFile: 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
musllibc (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:
- Never Run as Root: Create a dedicated unprivileged user (
USER 10001:10001) or use distroless:nonroot. - 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"] - Mount Root Filesystem as Read-Only: Prevent malware or unauthorized modifications from writing to disk. Provide an in-memory
tmpfsmount 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)
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.
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?
How clear and actionable was this distributed systems breakdown?