Verified against ChatGPT · 2026-08-08
Turn a working Dockerfile into a multi-stage build that doesn't ship your build toolchain to production
Rewrites a single-stage Dockerfile into a hardened multi-stage build with a minimal runtime image, a non-root user, and an explicit rationale for every layer decision, so image size and attack surface actually get reviewed instead of copy-pasted from a tutorial.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are a senior software engineer rewriting a Dockerfile for production use, not for a quick local demo. I'll give you the current Dockerfile (or a description of the app if none exists yet) and the runtime constraints, and you produce a multi-stage version plus a short review of what you changed and why. CURRENT DOCKERFILE OR APP DESCRIPTION FROM node:20 WORKDIR /app COPY . . RUN npm install RUN npm run build CMD ["node", "dist/server.js"] RUNTIME (language/framework, version) Node.js 20 LTS, Express API, no native addons BASE IMAGE CONSTRAINT Must come from our internal Artifactory mirror of official Docker Hub images; Alpine variants preferred for size but must pass Trivy scan with zero criticals SECRETS OR BUILD-TIME ARGS IN USE A read-only npm token for our private @acme scope, currently passed as a plain ARG (this is the bug we're fixing) DEPLOYMENT TARGET AWS ECS Fargate, tasks get SIGTERM on deploy with a 30-second grace period RULES FOR THE REWRITE Separate the build stage from the runtime stage explicitly — compilers, dev dependencies, and build tools must never appear in the final layer. Pin the base image to a specific digest or exact version tag, never `latest`, and justify the choice against the base image constraint given. Run the final container as a non-root user created in the Dockerfile, not root with a comment saying to fix it later. If build-time secrets are involved, use BuildKit secret mounts or multi-stage copy tricks to keep them out of any layer's history — never bake a credential into an `ENV` or `ARG` that persists in the image. Order instructions so that the least-frequently-changing layers (dependency installation) come before the most-frequently-changing ones (application source) to maximize build cache hits. Add a `HEALTHCHECK` instruction appropriate to the deployment target, and explain why you picked its interval and retry values rather than using defaults with no reasoning. WHAT NOT TO DO Do not add unrelated optimizations the user didn't ask about (multi-arch builds, distroless conversion) without flagging them as a separate optional suggestion at the end, clearly separated from the required rewrite. Do not silently drop a system dependency the original Dockerfile installed without confirming it's actually unused by the runtime stack given. OUTPUT FORMAT 1. The full rewritten Dockerfile in a single code block. 2. A short table: layer/instruction, what changed, why. 3. One paragraph on the resulting image size trade-off and any caching implications. 4. A separate 'optional, not applied' list for anything you noticed but didn't change unprompted.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
The rewrite works because it forces the model to reason about the two Dockerfiles that actually matter — the one that builds and the one that ships — as separate artifacts with separate concerns, which is exactly the distinction most tutorial-derived Dockerfiles skip; a single-stage build has no structural way to keep a compiler or dev-dependency tree out of the final image, so asking generically to 'optimize' a Dockerfile without naming the multi-stage requirement tends to produce cosmetic changes like combining RUN lines rather than the actual attack-surface reduction. Pinning the base image to a digest instead of a tag matters mechanically because `latest` and even semver tags are mutable pointers — a rebuild six months later can silently pull a different image with different CVEs, which defeats any scanning done at review time; naming this explicitly stops GPT-5.1 from defaulting to the more common but weaker `FROM node:20-alpine` pattern it has seen far more often in training data than the pinned-digest form. The non-root user requirement addresses a specific, checkable failure mode: a container running as root inside is one kernel-level container-escape bug away from root on the host, and this is exactly the kind of finding that gets deferred as 'we'll fix it later' unless the Dockerfile is rejected for missing it up front. The layer-ordering instruction is a direct lever on build cache economics — Docker invalidates every layer after the first changed one, so copying source before installing dependencies means every single build reinstalls all dependencies from scratch, which compounds into real CI minutes and cost at scale; stating the ordering rule explicitly prevents the model from defaulting to whatever order the original Dockerfile happened to use.
What you get back
```dockerfile # ---- build stage ---- FROM node:20.11.1-bookworm-slim@sha256:abcd... AS build WORKDIR /app COPY package*.json ./ RUN --mount=type=secret,id=npm_token \ NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci COPY . . RUN npm run build # ---- runtime stage ---- FROM node:20.11.1-bookworm-slim@sha256:abcd... AS runtime RUN useradd --uid 1001 --create-home appuser WORKDIR /app COPY --from=build --chown=appuser:appuser /app/dist ./dist COPY --from=build --chown=appuser:appuser /app/node_modules ./node_modules USER appuser HEALTHCHECK --interval=15s --timeout=3s --retries=3 CMD node healthcheck.js CMD ["node", "dist/server.js"] ``` Layer table follows, noting the npm token now never touches a committed layer and the image dropped from 1.1GB to roughly 240MB.
Verified against
ChatGPT GPT-5.1 · 2026-08-08
Changelog
- 2026-08-08 — Initial publish, verified against ChatGPT GPT-5.1.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
