Docker containers package application code, runtime, libraries, and configuration into a portable, immutable image built from a Dockerfile. Images are composed of union-filesystem layers — each RUN, COPY, and ADD instruction creates a layer; layers are cached and shared across images, making builds fast and registries space-efficient. Multi-stage builds separate build tooling from the final runtime image, dramatically reducing image size and attack surface. Images are distributed via registries: ECR (AWS), ACR (Azure), GCR/Artifact Registry (GCP), or Docker Hub.

Key Points

  • Image layers are content-addressed (SHA256 digest) and cached: changing a layer invalidates all subsequent layers — place frequently changing instructions (COPY source code) after stable layers (RUN apt-get install).
  • Multi-stage build example: use `golang:1.22` to compile binary, then `FROM scratch` or `FROM gcr.io/distroless/static` for the final image — reduces a Go image from ~800 MB to ~10 MB.
  • Docker BuildKit (default since Docker 23) supports parallel stage execution, secret mounts (`--mount=type=secret`), and SSH forwarding — never use ARG to pass secrets.
  • Container image scanning: ECR Basic Scanning (Trivy-based) or ECR Enhanced Scanning (Inspector-based) identifies CVEs in OS packages and language dependencies on push.
  • OCI (Open Container Initiative) image specification and runtime specification ensure portability — containerd, CRI-O, and Docker all implement the OCI runtime spec.
  • Distroless images (Google) contain only the application runtime (no shell, no package manager) — dramatically reduces attack surface and CVE count.
  • Registry replication: ECR cross-region replication and cross-account replication ensures images are available near compute without pulling across regions (latency + egress cost).
  • Image tag immutability: enable ECR image tag immutability to prevent tag overwriting — critical for reproducible deployments and audit trails.

Multi-stage Docker build: separate build and runtime stages, non-root user, health check

# Multi-stage Dockerfile: Node.js API
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine AS runtime
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=appuser:appgroup src/ ./src/
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "src/index.js"]

Real-World Example

Spotify builds ~250,000 Docker images per month using Bazel + Docker BuildKit, with ECR as the registry; images average 45 MB using distroless base images, down from 600 MB with full Debian bases.