16.1.1.5 Build Path Confusion
A focused guide to Build Path Confusion, connecting core concepts with practical Docker and container operations.
Build path confusion arises from how differently various Dockerfile instructions resolve relative paths, since COPY, RUN, WORKDIR, and ENTRYPOINT each have their own rules for what a relative path is actually relative to, and conflating these rules, often by assuming they all behave like a single, consistent shell working directory, is a frequent source of files ending up somewhere unexpected.
WORKDIR sets the current directory for subsequent instructions
WORKDIR changes the effective current directory for every instruction that follows it within that build stage, and unlike a shell's cd, it persists across separate instructions rather than being scoped to a single RUN line:
WORKDIR /app
RUN pwd # /app
COPY . . # copies into /app
WORKDIR src
RUN pwd # /app/src, since WORKDIR resolves relative to the previous WORKDIR
A relative path passed to WORKDIR itself resolves relative to whatever the previous WORKDIR was, which means a sequence of relative WORKDIR instructions builds up a path incrementally rather than each one resolving from the filesystem root.
COPY destination resolution
A relative destination path in COPY resolves relative to the current WORKDIR, while the source path resolves relative to the build context root, which means source and destination in the same COPY instruction can be relative to two entirely different base locations:
WORKDIR /app
COPY config/settings.json ./settings.json
Here, config/settings.json (the source) resolves relative to the build context root, regardless of WORKDIR, while ./settings.json (the destination) resolves relative to /app, the current WORKDIR; this asymmetry, source relative to context, destination relative to WORKDIR, is a common point of confusion for anyone assuming both sides of a COPY instruction share the same base.
RUN executes with WORKDIR as its shell's current directory
A RUN instruction executes its command with the current WORKDIR as the shell's actual working directory, which means relative paths used within the command itself behave exactly as they would in an interactive shell session started from that directory:
WORKDIR /app
RUN mkdir logs && touch logs/app.log
This creates /app/logs/app.log, which is usually intuitive, but becomes a source of confusion specifically when a RUN instruction's relative path assumption does not match what a developer expects because they were thinking in terms of the repository's own directory layout rather than the container's current WORKDIR at that point in the build.
ENTRYPOINT and CMD path resolution at runtime, not build time
Unlike COPY, RUN, and WORKDIR, which all execute and resolve paths during the build itself, ENTRYPOINT and CMD define what runs when a container starts, and any relative paths within them resolve relative to the WORKDIR in effect at container start time, which is whatever WORKDIR was last set in the Dockerfile, carried forward into the running container:
WORKDIR /app
ENTRYPOINT ["./start.sh"]
This resolves ./start.sh relative to /app at container runtime, which works correctly only if start.sh was actually copied to /app/start.sh during the build; a mismatch between where a file was copied during the build and where the runtime WORKDIR expects it to be is a common cause of an "exec format error" or "no such file or directory" failure that only appears when the container actually starts, not during the build itself.
Multi-stage builds reset WORKDIR per stage
Each FROM instruction begins a new stage with its own, independent WORKDIR, defaulting to the filesystem root unless explicitly set again; a WORKDIR set in an earlier stage has no effect on a later stage, even when copying files between them:
FROM node:20 AS build
WORKDIR /build
RUN npm run build
FROM nginx:alpine
COPY --from=build /build/dist /usr/share/nginx/html
Note that the COPY --from=build instruction in the second stage must reference the absolute path /build/dist, since the second stage's own WORKDIR (the nginx image's default, not /build) has no relationship to the first stage's WORKDIR at all; relative paths in a COPY --from= source are relative to the referenced stage's own filesystem root context, not to that stage's WORKDIR setting, which is itself a subtlety worth being explicit about.
ARG and ENV interpolation in paths
Build arguments and environment variables can be used within path expressions, but their resolution timing, at build time for ARG, persisting into the running container for ENV, affects when and where the interpolated value actually takes effect:
ARG APP_DIR=/app
WORKDIR ${APP_DIR}
COPY . .
ENV APP_DIR=/app
WORKDIR ${APP_DIR}
Both work for setting WORKDIR during the build, but only the ENV version makes APP_DIR available as an environment variable inside the running container afterward, which matters if application code or a startup script also needs to reference that same path value at runtime, not just during the build.
Path separator assumptions across platforms
For Dockerfiles intended to build correctly on both Linux-based images (the overwhelming majority) and, less commonly, Windows container images, path separator conventions differ, and a Dockerfile written assuming Unix-style forward slashes will not work correctly against a Windows-based base image expecting backslashes:
WORKDIR /app
WORKDIR C:\\app
This is a narrower concern than most of the path confusion described above, relevant specifically when targeting Windows containers, but worth being aware of explicitly if a project's build matrix ever needs to support both.
Common mistakes
- Assuming a
COPYinstruction's source and destination both resolve relative to the same base, missing that source resolves against the build context while destination resolves against the currentWORKDIR. - Forgetting that
WORKDIRpersists across instructions within a stage but resets entirely at the start of each new stage in a multi-stage build. - Writing an
ENTRYPOINTorCMDwith a relative path that assumes aWORKDIRdifferent from whatever was actually last set in the Dockerfile, causing a runtime, not build-time, failure. - Using
ARGfor a path value that the running container's own code or scripts also need to reference, whenENVwould have made the value available at runtime as well. - Not accounting for path separator differences when a build matrix needs to target both Linux and Windows container images.
Build path confusion is resolved by treating each Dockerfile instruction's path resolution rules as genuinely distinct rather than assuming a single, shared notion of "current directory" applies uniformly throughout the file, and being explicit, using absolute paths where ambiguity is possible, removes most of the guesswork that leads to files ending up in an unexpected location.