After this lesson, you will be able to: Apply multi-stage builds, optimize layer caching, minimize image size, and integrate security scanning into the build.
The difference between a 1.5GB Docker image and a 50MB one is one or two file edits. Production images should be small, fast, and signed.
Build in one stage with full tooling; copy only the artifact into a slim final stage.
# syntax=docker/dockerfile:1# Stage 1: build (has compilers, dev deps, source)FROM node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build# Stage 2: runtime (only what you need to RUN)FROM node:20-alpineWORKDIR /appENV NODE_ENV=productionCOPY package*.json ./RUN npm ci --omit=devCOPY --from=builder /app/dist ./distUSER nodeEXPOSE 3000CMD ["node", "dist/server.js"]# Final image contains: alpine OS + node runtime + production deps + your dist/.# No source code, no devDependencies, no build tooling. Often 5-10x smaller.
node:20 → about 1GB (Debian-based, lots of tools). node:20-slim → about 200MB (smaller Debian). node:20-alpine → about 150MB (Alpine Linux, musl libc). node:20-alpine + multi-stage → about 100MB total. distroless (gcr.io/distroless/nodejs20-debian12) → no shell, minimal attack surface, often <100MB. Used by Google in production. Alpine gotcha: musl libc differs from glibc; some native modules (sharp, bcrypt) need extra setup. If you hit weird errors, try slim before chasing the alpine error.
Same rule as the Dockerfile lesson: least-changing first, most-changing last. Combine related RUN commands: `RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*` is one layer; three RUNs would be three. Use BuildKit cache mounts for package caches: `RUN --mount=type=cache,target=/root/.npm npm ci`. Massively faster local builds. Avoid `COPY . .` early. Use a `.dockerignore` that excludes `node_modules`, `.git`, `tests/`, `*.md`.
Three habits that turn casual images into production-grade.
# In Dockerfile: never run as rootRUN addgroup -S app && adduser -S app -G appUSER app# Scan the built imagetrivy image myapp:1.0docker scout quickview myapp:1.0 # built into Docker Desktop# Sign the image (Cosign, supply-chain security)cosign sign --key cosign.key myapp:1.0# Receiver can verify: cosign verify --key cosign.pub myapp:1.0# In CI:# - run Trivy on every PR# - fail the build on Critical CVEs# - sign on every release tag
Single-stage builds with full toolchain in production. Bloated + insecure. `COPY . .` then `RUN rm -rf node_modules`. Doesn't shrink the layer; the deleted file is still in the previous layer. Use multi-stage or .dockerignore. Running as root. Most CVEs assume non-root containers; running as root multiplies the blast radius. Skipping security scans. Outdated base images are the #1 source of CVEs in container deploys. Trusting `:latest` of any base image. Pin to a specific version + digest for reproducibility.
Pick the cleanest answer.
Sign in and purchase access to unlock this lesson.