skip to content

What does WORKDIR do in a Dockerfile, and why does `RUN cd /app` not have the same effect?

level: juniorimportance: must knowfreq 64%

answer

  1. persists across instructions; cd does not
  2. each RUN = new shell, new layer
  3. relative WORKDIR appends to previous
  4. auto-created: mkdir+chown yourself if non-root writes
  5. SHELL swaps /bin/sh -c, pipefail trick

basics

~20 s

WORKDIR sets the working directory for every following instruction and for the container's main process, and creates it if missing. Each RUN runs in its own shell, so a cd inside one RUN is forgotten when that step ends.

solid answer

~50 s

WORKDIR sets the current directory for subsequent RUN, CMD, ENTRYPOINT, COPY and ADD instructions, and it is persisted in the image config as Config.WorkingDir, so the container starts there too. It creates the directory if it does not exist. `RUN cd /app` only changes the directory of the shell that step spawned. Every RUN is a separate command in a separate container layer, so the next instruction is back where it started. Within one RUN you can chain with `cd /app && make`, but across instructions only WORKDIR persists. Paths are cumulative: a relative WORKDIR is resolved against the previous one, and the base image's WORKDIR is inherited, so always use absolute paths to stay predictable. WORKDIR can reference ENV or ARG values. At runtime `docker run -w /other` overrides it. Do not rely on the ownership of a directory that WORKDIR auto-creates: mkdir and chown it explicitly if a non-root process must write there.

code

dockerfile · 6 lines
dockerfile
FROM node:22-alpine
RUN cd /srv && npm --version     # cwd change lost when this step ends
WORKDIR /srv/app                 # created if missing, applies from here on
COPY package*.json ./            # -> /srv/app/package.json
RUN npm ci                       # runs in /srv/app
CMD ["node", "server.js"]        # container starts in /srv/app

go deeper

for a junior

Say what WORKDIR sets, that it creates the directory, and that each RUN is its own shell so cd does not persist.

for a middle

Add path composition with the base image, variable expansion, effect on relative COPY destinations, and the -w runtime override.

for a senior

Tie it to ownership with USER, reproducibility of builds, and using SHELL with pipefail so silent pipeline failures do not ship.

for a principal

Treat it as image-convention hygiene: one predictable absolute app root across base images, so tooling, entrypoints and debugging behave uniformly.

## The instruction `WORKDIR /path` sets the working directory used by every following RUN, CMD, ENTRYPOINT, COPY and ADD instruction, and stores the final value in the image configuration as Config.WorkingDir. When the container starts, its main process has that directory as its cwd. If the path does not exist, the builder creates it, including parents, even if no later instruction uses it. ## Why cd does not stick A Dockerfile is not a script executed in one long-lived shell. Each RUN starts a fresh process (by default `/bin/sh -c <command>`) in a fresh container built from the previous layer; when the command exits, its process state - cwd, environment set by `export`, background jobs - dies with it. Only filesystem changes are captured in the layer. So `RUN cd /app` changes a directory for the lifetime of one shell and nothing more. Chaining inside a single RUN works (`RUN cd /app && ./configure && make`) because it is one shell; across instructions you need WORKDIR. ## Relative paths and inheritance WORKDIR values compose. `WORKDIR /usr/src` followed by `WORKDIR app` puts you in /usr/src/app. The base image may already set one, so a relative WORKDIR in your Dockerfile lands somewhere that depends on FROM. Absolute paths avoid the surprise. Variables are expanded, so `ENV APP_HOME=/app` then `WORKDIR $APP_HOME` is valid, as are build args. ## Ownership and permissions Auto-created directories are created by the builder, and their ownership has varied between the classic builder and BuildKit versions. If a non-root process must write into that directory, do not depend on the default: while still root, `RUN mkdir -p /app/data && chown 10001:10001 /app/data`. This pairs with USER, which changes identity but not the ownership of anything already in the image. ## Relationship to COPY With a WORKDIR set, `COPY . .` copies the build context into that directory - a common source of confusion when someone expects the image root. Relative destinations in COPY and ADD are resolved against the current WORKDIR. ## Runtime override and inspection `docker run -w /somewhere-else image` overrides Config.WorkingDir for that container without rebuilding. Inspect the baked value with `docker inspect --format '{{.Config.WorkingDir}}' image`. ## The related SHELL instruction Shell-form RUN, CMD and ENTRYPOINT are wrapped by a default shell: `["/bin/sh", "-c"]` on Linux. SHELL replaces that wrapper for the instructions that follow. The most common reason is failure semantics: in `RUN a | b`, sh reports only the exit status of the last command, so a failure in `a` is silently swallowed. `SHELL ["/bin/bash", "-o", "pipefail", "-c"]` makes the build fail properly - it requires bash to exist in the image, which Alpine's default busybox sh does not provide. SHELL is also how you switch to powershell on Windows base images. Like WORKDIR, it applies only downward from where it appears and can be set more than once.

  • What does the SHELL instruction change, and when is it worth using?
    It replaces the default shell used to wrap shell-form RUN, CMD and ENTRYPOINT, which is /bin/sh -c on Linux. The classic use is SHELL ["/bin/bash", "-o", "pipefail", "-c"] so a failure in the middle of a pipeline fails the build instead of being masked by the last command's exit code. It requires the chosen shell to exist in the image and only affects instructions written after it.
  • You set WORKDIR /app and then USER 10001, and the app cannot write to /app. Why?
    WORKDIR only sets the cwd and creates the directory; it does not grant the runtime UID write permission on it. Create and chown the directory explicitly while still root, before switching users, or mount a writable volume with the right ownership.

saying these in an interview costs you the question

  • Believing a cd or an export in one RUN carries into later instructions
  • Using relative WORKDIR values and assuming the base image starts at /
  • Assuming a WORKDIR-created directory is owned by the USER you set
  • Thinking WORKDIR only affects RUN and not COPY destinations or the container's start directory

context