A build environment and a runtime environment have different jobs. Compilers and development tools may be essential for producing an application without belonging in the final image.

That is the useful idea behind my original container-hardening article. Its Dockerfiles also contain two dependency-order problems worth calling out instead of polishing past them.

01 / FIELD NOTESStart with what is copied

The first published example puts the whole build context in the image:

Original single-stage example · historical / Dockerfile
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

Without suitable exclusions, that context can contain material the application does not need. The source specifically called out .env, .git, and development dependencies.

The example's size is not a universal property of this Dockerfile. The actual context, dependency versions, base image, architecture, and build determine the result.

02 / FIELD NOTESA second stage is not a dependency filter

The original multi-stage example was:

Original multi-stage example · full dependency tree copied / Dockerfile
# build stage
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# runtime stage
FROM node:20-slim AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

It separates the build environment from a slimmer runtime. But it runs npm ci in the build stage and copies that stage's entire node_modules directory into the runtime. It does not show a production-only dependency installation or pruning step.

Technical check: npm's omit documentation explains which dependency types are installed on disk. With the ordinary install behavior shown here, a stage boundary alone does not make the copied tree production-only. An external environment setting or a different dependency layout would need separate evidence.

SOURCE REVIEW / BUILD & RUNTIME

A boundary is not a filter.

Compare the original patterns, then inspect a separately authored conceptual sequence. No Docker build runs here.

BUILD ENVIRONMENTtools availableThe exact package manifest still matters.
WHAT REACHES THE RUNTIME
compiled outputproduction dependenciesdev dependencies

The stage boundary does not filter node_modules.

The original runtime copies the installed dependency tree wholesale.

The new separate-step mode is a conceptual explanation, not a tested replacement Dockerfile. Source excerpts remain unchanged below and above this figure.
Read the figure without interaction

A single-stage copy retains the build context. The original multi-stage excerpt copies the complete installed node_modules tree. The early-omit excerpt can lack a needed development build tool. A separately assembled runtime can limit dependencies, but must be tested with the real app. No image size or vulnerability count is derived here.

The figure compares the source patterns with a separately authored conceptual sequence. It is not running Docker and does not calculate image sizes or vulnerability counts.

03 / FIELD NOTESOmitting build tools too early can break the build

The original distroless example takes a different route:

Original distroless example · build dependency caveat / Dockerfile
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build

FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER nonroot
CMD ["dist/server.js"]
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

It runs npm ci --omit=dev before npm run build. If the build needs a compiler or bundler declared as a development dependency, that tool may be missing when the build starts.

The actual project manifest is not supplied. It would be wrong to declare that every project using this pattern fails, or to call this a verified working recipe for Atlas. Select the build-dependency scenario in the figure to see the specific condition being discussed.

A separate build and runtime-dependency step is one way to make the responsibilities clearer. The final files still need to match the app, including native-module compatibility and any runtime assets. Docker's multi-stage guide explains the boundary; it does not decide those project-specific details for you.

04 / FIELD NOTESDistroless changes the contents, not the need for security work

Distroless documents its normal images as lacking a shell, with separate debug variants. That reduces available tooling, but it is not proof that an exploited application cannot run code, access data, or make network requests.

Similarly, a non-root user is useful hardening, not the removal of every privilege-escalation path. The application, runtime, dependencies, permissions, and surrounding platform still need attention.

The Node 20 / Debian 12 tags above are retained historical examples, not a current base-image recommendation. Choose a supported runtime for the real application and verify it before adopting a new image.

05 / FIELD NOTESWhat the original measurements do—and do not—show

The June article and Q2 log reported a change from about 1.1 GB to about 180 MB, and fewer known findings in a Trivy scan. The supplied materials do not include the image digests, build outputs, scan database dates, or complete scan results needed to reproduce those claims.

Keep the figures attached to the historical account. Do not turn them into a new benchmark, a guaranteed cold-start improvement, or a declaration that the image has no exploitable weaknesses.

06 / FIELD NOTESTakeaways for the next build

Inspect the build context before the first broad COPY. Make the build inputs explicit. Decide which dependencies and assets the runtime needs. Then test the actual image rather than assuming that a smaller base is compatible.

Digest pinning can make a base reference reproducible, but the digest must be updated intentionally as fixes become available. Docker's build guidance covers that maintenance tradeoff as well as context exclusions and rebuilding.

Scan the resulting image and review the findings under an explicit policy. Re-run the application tests. The goal is a runtime whose contents you can explain—not a small file presented as a security guarantee.

See the Q2 log for the original dated account behind the size claim.

Source, dates, and what this page verifies

Based on the complete supplied project/article account. Original publication dates and source URLs are retained. This is an edited reference, not a published update or a new audit of the original application.

Official technical clarifications checked September 29, 2026 are explicitly marked and linked beside the relevant passages. They are distinct from the original author's historical claims. The jq and Docker excerpts remain unchanged and carry their source limitations.

New interactive examples are bounded local teaching tools; they do not scan a website, process credentials, contact AWS, build an image or report a production measurement. No repository, staff account, service terms, résumé, price or live backend was changed.

See docs/batch-6/SOURCE-REVIEW.md for the separation of source-derived claims, external documentation and authored illustrations.

Original published source ↗