Haliviq
Docker

.dockerignore doesn't apply to COPY --from another stage

We kept finding .env in the final image because .dockerignore only filters the build context sent to Docker, not files copied between stages. Be explicit about what you copy in later stages, never copy the whole directory, and inspect the final image to confirm no secrets are inside.

What happened

We kept finding a .env file inside a final image, even though we believed .dockerignore was keeping it out. It was a worrying result, because that image was about to be pushed to a registry.

The file turned up through a later stage that copied a whole directory from an earlier stage, so our ignore rules never got a chance to apply.

Why it happens

The .dockerignore file filters the build context, which is the set of files the client sends to the builder for instructions like COPY . . and ADD. It does not describe the filesystem of a stage. COPY --from=<stage> reads from that stage or image directly, so the ignore file plays no part.

That means anything that did get into the earlier stage can travel on. A file might get there because a pattern did not match (a bare .env only matches at the context root, so nested ones need **/.env), because a RUN step generated it, or because it was already in a base image. Copying a whole directory then carries all of it along.

What we do now

In later stages we now copy specific paths, such as the build output directory and the package manifest, and never a whole app directory. We also tightened .dockerignore, using **/.env* so nested files are covered, but we treat that as the first line of defence rather than the only one.

Finally, we inspect the result. Listing files in the final image takes seconds and tells us whether something slipped through. Also, secrets needed at build time should be passed with BuildKit secret mounts instead of being copied into any stage.

Dockerfile (final stage)dockerfile
FROM node:20-slim
WORKDIR /app
# Explicit paths only. Not: COPY --from=build /app .
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules

# Verify afterwards:
# docker run --rm --entrypoint ls my-image -la /app

Quick checklist

  1. 1List the exact paths the runtime needs and copy only those.
  2. 2Avoid COPY --from=build /app . in the final stage.
  3. 3Use **/.env* in .dockerignore so nested env files are excluded too.
  4. 4Run the finished image and list /app to confirm no secrets are present.

Rule of thumb

Treat .dockerignore as a context filter only, and be explicit about every path you copy between stages.

We'd love to hear what you're building.

Talk to us