1. Non-root user — never run as root. 2. Minimal base image — fewer packages = smaller attack surface. 3. No build tools in final stage — compilers and package managers should never ship to production.
🔐 Multi-Stage Builds, Security and Image Optimization
Build production images that are small, secure, and fast to pull — using multi-stage builds, distroless images, non-root users, and vulnerability scanning.
🧒 Simple Explanation (ELI5)
Building software requires lots of tools — compilers, test runners, linters. But your production server does not need any of that. Multi-stage builds are like a factory with separate rooms: one room does the messy build work (all the heavy tools), then passes only the finished product to a spotless clean room for shipping. The customer only receives the clean room output.
🔧 Multi-Stage Build Pattern
# ---- Stage 1: Builder ---- # Contains all build tools, dev dependencies, compilers FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci # install ALL deps including devDeps COPY . . RUN npm run build # compile TypeScript, bundle assets RUN npm prune --production # remove devDependencies # ---- Stage 2: Production ---- # Only the compiled output and runtime — no build tools FROM node:18-alpine AS production # Security: non-root user RUN addgroup -S app && adduser -S app -G app WORKDIR /app # COPY only the built artifacts from builder stage COPY --from=builder --chown=app:app /app/dist ./dist COPY --from=builder --chown=app:app /app/node_modules ./node_modules COPY --from=builder --chown=app:app /app/package.json . USER app EXPOSE 8080 CMD ["node", "dist/server.js"]
📊 Size Comparison
~800 MB
~120 MB
~70 MB
💻 Vulnerability Scanning
# Scan with Trivy (open source) docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy image myapp:1.0 # Scan with Docker Scout (built into Docker Desktop) docker scout cves myapp:1.0 # ACR built-in vulnerability scanning (Microsoft Defender for Containers) az acr task run --registry myacr --name scan-task # In CI — fail build if HIGH or CRITICAL vulnerabilities found trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:1.0
# Using distroless base (no shell, no package manager — minimal attack surface) FROM gcr.io/distroless/nodejs18-debian12 AS production # No RUN possible — distroless has no shell # COPY from builder directly COPY --from=builder /app/dist /app/dist COPY --from=builder /app/node_modules /app/node_modules USER nonroot CMD ["/app/dist/server.js"]
🐛 Debugging Scenario
Problem: ACR vulnerability scan shows 47 HIGH CVEs in your production image on nginx base.
# Step 1: identify which packages have CVEs trivy image --severity HIGH,CRITICAL nginx:1.24 # Step 2: upgrade to latest patch version # Change FROM nginx:1.24 to FROM nginx:1.25.3 (latest stable) # OR use Alpine variant for smaller attack surface: FROM nginx:1.25-alpine # Step 3: pin base image digest for reproducibility FROM nginx:1.25.3@sha256:abc123... # immune to tag mutation # Step 4: add to CI pipeline — automatic fail on critical CVEs # trivy image --exit-code 1 --severity CRITICAL myapp:latest
🎯 Interview Questions
A multi-stage Dockerfile uses multiple FROM instructions — each starts a new stage with its own filesystem. You copy only specific artifacts from earlier stages using COPY --from=stagename. The final image contains only what was in the last stage. Benefits: 1) Dramatically smaller images (build tools excluded). 2) Better security (no compilers, package managers, or test frameworks in production). 3) Build secrets used in early stages never reach the final image.
Distroless images contain only the application runtime and its dependencies — no shell (bash/sh), no package manager, no coreutils. Maintained by Google (gcr.io/distroless). Benefits: smallest possible attack surface, no shell means attackers have nothing to exec into. Drawback: harder to debug at runtime — you cannot exec into a distroless container shell. Use debug variants during development.
1. Add Trivy or Docker Scout to your CI pipeline after the build step. 2. Configure it to exit with code 1 on CRITICAL severity: trivy image --exit-code 1 --severity CRITICAL. 3. This fails the pipeline and blocks the push. 4. Set up base image auto-update: Dependabot or Renovate Bot can automatically open PRs when new base image versions are published. 5. Enable ACR Content Trust for signed image verification. 6. Track CVE remediation in your backlog with SLA (e.g., patch CRITICAL within 24h).
📋 Summary
- Multi-stage builds produce small, secure images — build tools stay in builder stages, production only ships the output.
- Typical reduction: 800MB → 120MB or less with multi-stage + Alpine.
- Distroless images remove all OS utilities — minimum attack surface.
- Scan images with Trivy, Docker Scout, or ACR Defender — fail CI on HIGH/CRITICAL CVEs.
- Pin base image versions (and optionally digests) for reproducible, auditable builds.