✦ For everyone, free.

Practical knowledge for real and everyday life

Home

16.1.1.1 Build Missing Files

A focused guide to Build Missing Files, connecting core concepts with practical Docker and container operations.

Build missing files describes the situation where a build completes successfully, with no error reported at any step, but a file the application expects at runtime is actually absent from the resulting image, a distinct and often more confusing problem than a build-time COPY failure, since nothing in the build output indicates anything went wrong.

Verifying what is actually in the final image

The most direct way to confirm whether a specific file made it into the final image is inspecting the image's filesystem directly, rather than relying on assumptions about what a given Dockerfile instruction should have produced:

docker run --rm my-api:1.4.0 ls -la /app
docker run --rm my-api:1.4.0 cat /app/config/settings.json
docker run --rm my-api:1.4.0 find / -name "settings.json" 2>/dev/null

Running this check immediately after a suspicious build, before spending time investigating the Dockerfile's logic in the abstract, often reveals quickly whether the file is genuinely missing, present but in an unexpected location, or present with unexpected content.

.dockerignore excluding a needed file accidentally

A common, easy-to-make mistake is a .dockerignore pattern that is broader than intended, excluding a file the application actually needs alongside the files it was meant to exclude:

*.json
COPY config/settings.json ./config/

A pattern like *.json excludes every JSON file from the build context, including a legitimately needed configuration file, not just the intended target (perhaps package-lock.json or some other specific file); the COPY instruction in this case fails silently from the application's perspective, since Docker's COPY does not error when a glob pattern in the instruction itself matches files that simply were not present in the context due to being excluded earlier.

package-lock.json
node_modules

A more specific .dockerignore pattern, naming exactly what should be excluded rather than a broad wildcard, avoids this class of accidental over-exclusion.

Multi-stage build copying from the wrong stage or path

In a multi-stage build, a file genuinely produced correctly in an earlier stage can still end up missing in the final image if the COPY --from= instruction references the wrong stage name or an incorrect path within that stage:

FROM node:20 AS build
RUN npm run build
# output actually lands in /app/build, not /app/dist

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

This build can succeed without error even though /app/dist does not exist in the build stage at all, depending on how COPY --from= handles a non-existent source path in the specific Docker version in use; verifying the actual output path inside the intermediate stage directly is the fastest way to catch this:

docker build --target build -t my-api:debug-build .
docker run --rm my-api:debug-build ls -la /app

Case sensitivity mismatches

On a case-insensitive host filesystem (common on macOS and Windows), a COPY instruction with a path that differs only in case from the actual file can succeed locally during development but fail once built on a case-sensitive Linux-based image, or vice versa, producing a missing file specifically in one environment but not another:

COPY Config/settings.json ./config/

If the actual directory on disk is named config (lowercase) but the Dockerfile references Config (different case), this works without complaint on a case-insensitive host filesystem during local builds but fails, or copies nothing, once the build runs in a case-sensitive context, which is a subtle source of "works on my machine" build inconsistency.

WORKDIR and relative path confusion

A file copied to a path relative to an unexpected current working directory, due to a WORKDIR instruction earlier in the Dockerfile changing what "relative" actually resolves to, can end up in a different location than intended:

WORKDIR /app
COPY settings.json ./config/settings.json

This copies the file to /app/config/settings.json, not to a config/ directory relative to the filesystem root or relative to wherever the Dockerfile author may have been assuming; explicitly using absolute paths in COPY destinations, or being deliberate about where WORKDIR has set the current context, avoids ambiguity about where a file actually ends up.

A later instruction overwriting or removing the file

A file correctly copied in one instruction can be removed or overwritten by a later instruction in the same Dockerfile, particularly in a long, evolved Dockerfile where an earlier COPY and a later cleanup step were written by different people at different times without full awareness of each other:

COPY config/ ./config/
RUN rm -rf ./config/*.tmp ./config/* # overly broad cleanup, removes everything in config/

Reviewing the full sequence of instructions for anything that broadly removes or overwrites a directory after the file in question was copied catches this category of issue, which is easy to miss when only the specific COPY instruction believed to be responsible is reviewed in isolation.

Build succeeding due to a cached layer masking the actual problem

A build that uses a cached layer for the step that copies the file in question can continue to report success even after the underlying source file has been deleted or moved, since the cached layer's output is reused without re-executing the instruction against current context contents:

docker build --no-cache -t my-api .

Rebuilding without cache specifically rules out this scenario, confirming whether the file would actually be successfully copied if that step were genuinely re-executed against the current state of the build context.

Common mistakes

  • Using an overly broad .dockerignore pattern that accidentally excludes a needed file along with the intended targets.
  • Referencing the wrong stage name or path in a multi-stage build's COPY --from= instruction without verifying the actual output location in the source stage directly.
  • Relying on case-insensitive path matching during local development, masking a case-sensitivity mismatch that only surfaces once built in a case-sensitive environment.
  • Writing an overly broad cleanup or removal instruction later in the Dockerfile that inadvertently deletes a file copied earlier.
  • Trusting a successful build result without ruling out a stale cached layer masking what would actually happen if the relevant step were re-executed against current context contents.

Build missing files problems are best resolved by directly inspecting the final image's actual filesystem contents rather than reasoning abstractly about what the Dockerfile should have produced, since the gap between intended and actual behavior in this category of issue is frequently subtle, an overly broad ignore pattern, a case mismatch, a wrong stage reference, that only direct inspection reliably catches.