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 Multi-Stage Build Optimization π¦
Compiling within a heavy development image and copying strictly the static binary to a minimal, non-root production container.
01.1. 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.2. 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.3. 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.4. 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 Assessment π§
Step through 3 scenario questions to test your staff-level grasp.
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 staff-actionable was this system breakdown?