A surprising number of container images ship with credentials baked directly into a layer — not because anyone meant to, but because Docker's layer model makes it easy to leak a secret without noticing.
Why "delete it later" doesn't work
Docker images are built as a stack of layers, and each RUN, COPY, or ADD instruction creates a new one. If a secret is copied into the image in one layer and deleted in a later layer, the secret is still recoverable — it just requires extracting the earlier layer, which any tool that can pull the image can do.
# This does NOT remove the secret from the final image
COPY .env /app/.env
RUN rm /app/.env
Use multi-stage builds to keep secrets out of the final image
Multi-stage builds let you use a secret during the build process (e.g., to pull a private package) without it ever reaching the final image, because only the artifacts you explicitly copy forward make it into the last stage.
FROM node:20 AS builder
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm install
FROM node:20-slim
COPY --from=builder /app/dist /app/dist
Use BuildKit's secret mount, not build args
ARG and ENV both persist secret values into image history. Docker BuildKit's --mount=type=secret flag makes a secret available only during a specific RUN instruction, without persisting it into any layer or the image metadata.
Scan images before pushing
Even with careful builds, image scanning as a backstop catches secrets that slip through — misplaced config files, debug artifacts, or a .env accidentally included via a broad COPY . ..
Use a proper .dockerignore
A missing .dockerignore is one of the most common causes of secrets ending up in images — COPY . . picks up .env, .git, and local credential files that were never meant to leave the developer's machine.
The fix isn't more vigilance during builds — it's a build pattern (multi-stage, BuildKit secrets, .dockerignore) that makes leaking a secret structurally difficult, not just discouraged.