Dash0 Raises $110M Series B at $1B Valuation

  • 10 min read

How to Update the PATH Variable in a Dockerfile

Use the ENV instruction with the existing PATH preserved: ENV PATH="/your/custom/bin:${PATH}". That single line adds your directory to PATH for every subsequent build step and for every process in containers started from the image.

If you're reading this, you probably installed a binary in a non-standard location and the container can't find it, or your RUN instruction works fine during the build but the change vanishes by the time the container runs. Both come from the same misunderstanding about what persists across Dockerfile instructions versus what evaporates the moment docker build finishes.

Set PATH with the ENV instruction

ENV writes a variable into image metadata. That metadata flows into every layer that follows, and into every container started from the final image. A minimal example:

dockerfile
1234567891011
FROM ubuntu:24.04
# Install something into a non-standard location
RUN mkdir -p /opt/mytool/bin && \
printf '#!/bin/sh\necho "Hello from mytool"\n' > /opt/mytool/bin/mytool && \
chmod +x /opt/mytool/bin/mytool
# Prepend the custom directory to PATH
ENV PATH="/opt/mytool/bin:${PATH}"
CMD ["mytool"]

Build and run it:

bash
12
docker build -t pathdemo .
docker run --rm pathdemo
text
1
Hello from mytool

Prepend rather than append when you want your binary to win. PATH is searched left to right and the first match wins, so ENV PATH="/opt/mytool/bin:${PATH}" makes your binary take precedence over anything with the same name elsewhere. The reverse form, ENV PATH="${PATH}:/opt/mytool/bin", only finds your binary if nothing earlier on PATH matched first.

Always quote the value and use the key=value form. Docker still accepts the legacy ENV PATH /opt/mytool/bin:$PATH (space instead of =), but the Dockerfile reference discourages it. For PATH the legacy form is genuinely risky: it treats everything after the variable name as the value, and a stray space lands you with a different variable name entirely.

Build-time PATH vs runtime PATH

Most PATH bugs in Dockerfiles come down to this: three things look like they set PATH, but only one of them actually does in a way that survives into the container.

ENV PATH=... sets PATH in image metadata. It applies to every subsequent RUN instruction in the build and to processes inside containers started from the final image. That's almost always what you want.

ARG is build-only. An ARG defines a build argument that doesn't persist in the final image, so setting PATH with ARG does nothing at runtime, even though it shadows ENV during the build. Nobody really wants this for PATH. People new to ARG sometimes reach for it because "argument" sounds more like configuration than ENV does.

RUN export PATH=... is the one that bites people. Each RUN starts a fresh shell; the export lives in that shell's environment and dies with it when the layer finishes. It's the single most common PATH mistake in Dockerfiles I've seen in code review, and it survives easily because nothing fails at build time:

dockerfile
12345
# This does NOT persist
RUN export PATH="/opt/mytool/bin:${PATH}" && \
do-something-with-mytool
# In a later RUN, and at runtime, /opt/mytool/bin is NOT on PATH

If you only need the modified PATH inside that one RUN, fine. If you need it after that, use ENV.

Debug PATH issues with docker exec printenv

When something isn't on PATH at runtime, the first move is to check what the container actually sees:

bash
12
docker run -d --name pathdemo pathdemo sleep 3600
docker exec pathdemo printenv PATH
text
1
/opt/mytool/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

If your directory is missing here, either the ENV instruction didn't take effect, or you set PATH inside a RUN and expected it to persist into runtime.

docker exec against a running container is the most reliable way to inspect environment, because it reflects everything the container went through, including any entrypoint script that further modifies PATH. The image-level config from docker inspect --format '{{.Config.Env}}' shows what was baked in by ENV, which can differ from what an entrypoint script produces by the time the main process starts.

When the directory is on PATH but a specific binary still isn't found, use which to confirm where it actually lives:

bash
1
docker exec pathdemo which mytool
text
1
/opt/mytool/bin/mytool

If which returns nothing, the binary isn't in any directory on PATH. Check that your COPY or RUN actually put it where you expected:

bash
1
docker exec pathdemo ls -la /opt/mytool/bin

For a one-shot inspection without a long-running container, drop into a shell directly:

bash
1
docker run --rm -it pathdemo sh -c 'echo $PATH && which mytool'

Common pitfalls

A few PATH issues only show up under specific conditions.

Exec form CMD doesn't expand variables. This Dockerfile builds cleanly but prints the literal string $PATH at runtime:

dockerfile
1
CMD ["echo", "$PATH"]

Exec form (the JSON array syntax) doesn't invoke a shell, so $PATH is passed verbatim to echo. If you need shell expansion in CMD or ENTRYPOINT, use the shell form (CMD echo $PATH) or invoke a shell explicitly: CMD ["sh", "-c", "echo $PATH"]. Exec form still finds binaries via PATH, so CMD ["mytool"] works fine; it just doesn't expand variables inside the argument list.

Multi-stage builds reset ENV between stages. Every FROM starts fresh. If you set PATH in the build stage and assume it carries into the runtime stage, it won't:

dockerfile
12345678
FROM ubuntu:24.04 AS builder
ENV PATH="/opt/mytool/bin:${PATH}"
# ... build steps ...
FROM ubuntu:24.04
COPY --from=builder /opt/mytool /opt/mytool
# PATH here is the default. You need ENV again.
ENV PATH="/opt/mytool/bin:${PATH}"

The build succeeds, the binary copies over, and the failure only shows up when something tries to invoke it by name instead of by absolute path. Same failure mode as RUN export: nothing complains until the container is already running.

.bashrc and .profile aren't sourced for non-interactive commands. RUN echo 'export PATH=...' >> ~/.bashrc looks plausible, but .bashrc is only sourced when an interactive bash shell starts. Most containers run a single non-interactive command and never source it. Use ENV for any PATH change that needs to be visible to the container's main process.

A trailing colon means the current working directory. ENV PATH="${PATH}:" adds an empty entry, which Unix treats as .. That's a real security concern, since anyone with write access to the working directory can shadow real binaries with their own. Make sure every colon has a real directory after it.

Final thoughts

ENV is the only mechanism that persists, and docker exec printenv shows you exactly what the container sees. Most PATH problems resolve from those two facts alone. Build-time changes that don't survive into runtime are almost always either an export inside RUN, or a multi-stage build that forgot to re-declare ENV. For when ENV values lock in versus when they can be overridden at startup, How to Start a Docker Container covers the create-vs-run mechanics.

Container environment problems have a habit of surfacing as something else failing for reasons that look unrelated. A missing binary causes an entrypoint to fall back to a default, the application starts in a degraded mode, and an alert fires three layers away from the actual cause. Without correlated Docker logs, metrics, and traces in one place, walking that chain backward can eat an afternoon. Dash0's OpenTelemetry-native infrastructure monitoring surfaces container metrics alongside real-time logs and distributed traces, so the binary that didn't run and the alert that did fire end up in the same view. Start a free trial to monitor your containers, pods, and clusters from one place. No credit card required.