Single-stage Docker builds include compilers, package managers, build caches, test fixtures, and development dependencies in the final production image. Every extra binary is an additional attack surface — a compromised container with build tools installed can compile exploits, exfiltrate files, or pivot to other services more easily. NIST 800-53 CM-7 requires systems to provide only essential capabilities. CIS Docker 4.9 explicitly requires minimal image construction. Images over 500 MB also inflate registry storage costs and slow deployment velocity at scale.
High because bloated images increase attack surface by including compilers and dev tools that have no role in production and enable post-compromise lateral movement.
Separate build and runtime stages in Dockerfile to exclude all build tooling from the final image:
# Stage 1: compile
FROM node:20 AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: runtime only
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/node_modules ./node_modules
USER 1001
CMD ["node", "dist/index.js"]
For Go or compiled binaries, the final stage can be FROM scratch or FROM gcr.io/distroless/static, reducing runtime images to under 20 MB.
ID: infrastructure-hardening.container-image-security.multi-stage-build
Severity: high
What to look for: Count all FROM statements in each Dockerfile to determine the number of build stages. For each Dockerfile, classify whether it uses multi-stage builds (at least 2 FROM statements with COPY --from between stages). Verify that build dependencies are not included in the final image and estimate final image size — production images should be under 500 MB.
Pass criteria: Every Dockerfile uses multi-stage builds where applicable (at least 2 FROM statements: a builder stage for compilation and a minimal final stage). Final image size is estimated under 500 MB for application containers, and build artifacts and development dependencies are excluded from the final layer. Report: "X of Y Dockerfiles use multi-stage builds."
Fail criteria: Any Dockerfile has a single stage with all dependencies (build tools, test files, source code) included in the final image, or final image exceeds 500 MB for an application container.
Skip (N/A) when: The project does not have a Dockerfile, or the application legitimately requires a large runtime environment.
Detail on fail: Quote the Dockerfile structure. Example: "Dockerfile is single-stage. Final image includes build dependencies (gcc, make, etc.) and source code. Estimated size: 800 MB" or "Only the builder stage is used; no COPY --from final stage detected"
Remediation: Multi-stage builds reduce image size by ~80%, lowering attack surface and deployment time:
# Stage 1: build
FROM node:20 AS builder
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2: runtime (minimal)
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /build/node_modules ./node_modules
COPY app.js .
USER 1001
CMD ["node", "app.js"]
This reduces a Node.js app from ~1GB (full image) to ~150MB (distroless + minimal dependencies).