skip to content

questions

5

In a Dockerfile, what is the difference between the ARG and ENV instructions — when does each value exist, and which of them is visible to a process inside the running container?

level: juniorimportance: must knowfreq 60%

answer

  1. ARG = build only, dies with the build
  2. ENV = baked into image config, seen by the container
  3. --build-arg vs docker run -e
  4. re-declare ARG in each stage; ENV beats ARG
  5. both readable: history / inspect

basics

~20 s

ARG is a build-time variable: it exists only while docker build runs, is set with --build-arg, and is gone at runtime. ENV is baked into the image, so it exists during the build and is present as an environment variable in every container started from the image.

solid answer

~50 s

**ARG** declares a build-time variable. It is set with `docker build --build-arg NAME=value` (or falls back to a default in the `ARG` line), is usable in `RUN`, `COPY` and even `FROM`, and its scope ends when the build ends. A container started from the image does **not** see it as an environment variable. **ENV** sets an environment variable that is recorded in the image config. It applies to the rest of the build — every later `RUN` sees it — and to every process in every container from that image, and it can be overridden per-container with `docker run -e`. Rule of thumb: things the build needs (a version to download, a proxy, a mirror URL) are ARG; things the application needs at runtime (`PATH`, `JAVA_OPTS`, `PORT`) are ENV. Neither is a secret store — `docker history` shows ARG values, and `docker inspect` shows ENV values.

code

dockerfile · 12 lines
dockerfile
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine

ARG APP_VERSION=0.0.0        # build input only
ENV APP_VERSION=${APP_VERSION}  # deliberately exposed at runtime
ENV NODE_ENV=production         # runtime config

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

go deeper

for a junior

State the core distinction cleanly: ARG is build-time and disappears; ENV is baked in and visible to the container, overridable with docker run -e.

for a middle

Add the scoping rules — per-stage declaration, position sensitivity, ENV precedence over a same-named ARG — and the ARG→ENV bridge pattern.

for a senior

Emphasise that neither is confidential (history/inspect), and route real secrets to build secret mounts or runtime secret injection.

for a principal

Turn it into a convention: which inputs are build-shaping versus runtime-configurable, so images stay environment-agnostic and one build artefact can be promoted across environments.

## Two different lifetimes A Dockerfile describes two distinct phases: the **build**, run by the builder to produce an image, and the **run**, when a container is created from that image. `ARG` and `ENV` sit on opposite sides of that line. **`ARG NAME[=default]`** declares a variable the *builder* knows about. Its value comes from `docker build --build-arg NAME=value`; if the flag is absent, the default in the `ARG` line is used; if there is no default either, the value is the empty string (not an error, which is why typos fail silently). From the point of the `ARG` line onward in that stage, `$NAME` is substituted in instructions such as `RUN`, `COPY`, `LABEL`, `ENV`, `EXPOSE` and `USER`. When the build finishes, the variable is gone — it is not written into the image's environment. **`ENV NAME=value`** writes an entry into the image's configuration (`Config.Env`). It has two effects: for the rest of the build, every subsequent `RUN` runs with that variable set in its environment; and for every container created from the image, the entrypoint process starts with it set. It is inherited by images that `FROM` this one. ## What each is visible to | | during build (`RUN`) | inside a running container | overridable at `docker run` | |---|---|---|---| | `ARG` | yes, after the `ARG` line | **no** | no | | `ENV` | yes, after the `ENV` line | yes | yes, with `-e` | The "during build" nuance matters: `ARG` puts the value into the *instruction text* through substitution, but it also injects it into the environment of `RUN` commands in the same stage. That is why `RUN echo $VERSION` works, and also why an ARG-supplied token is visible to any process (or malicious postinstall script) that a `RUN` step starts. ## Scope rules that trip people up - **Per-stage.** An `ARG` is scoped to the build stage it is declared in. In a multi-stage Dockerfile you must re-declare `ARG NAME` in each stage that uses it (the value carries over; only the declaration must be repeated). - **Position matters.** `ARG` and `ENV` take effect only from the line where they appear downward. Referencing a variable above its declaration yields an empty string. - **ENV wins over ARG for the same name.** If both `ARG FOO` and `ENV FOO` define the same name, the `ENV` value takes precedence for the rest of the build — a documented and frequently surprising rule. - **Predefined ARGs.** The builder pre-declares a handful without you declaring them: `HTTP_PROXY`, `HTTPS_PROXY`, `FTP_PROXY`, `NO_PROXY`, `ALL_PROXY` (and lowercase forms), plus platform args like `TARGETPLATFORM`, `TARGETARCH`, `BUILDPLATFORM` when using BuildKit. Proxy args are also excluded from `docker history` output. ## Which to use for what Use `ARG` for inputs that shape *how the image is built*: a dependency version to download, a base-image tag, a package mirror, a build profile, `TARGETARCH` for cross-compilation. Use `ENV` for configuration the *application* reads: `PATH` additions, `NODE_ENV`, `PORT`, `LANG`, JVM options. A common bridge pattern makes a build input available at runtime deliberately: ```dockerfile ARG APP_VERSION=0.0.0 ENV APP_VERSION=${APP_VERSION} ``` Now the value is supplied at build time but readable by the process. Do this consciously — it is exactly what you do **not** want for a token. ## Neither one is a secret mechanism Both are inspectable in the finished image. `docker history --no-trunc <image>` shows build-arg values that were substituted into instructions, and `docker inspect` prints the full `Env` list, including anything set with `ENV`. Anyone who can pull the image can read them. Credentials must come from a build secret mount (`RUN --mount=type=secret,...`) at build time, or from the runtime platform's secret injection at run time. ## Overriding at run time `ENV` values are defaults. `docker run -e NAME=value`, `--env-file`, or a Compose `environment:` entry overrides them for that container without changing the image. There is no equivalent for `ARG` — you cannot pass a build arg to `docker run`, because the build is long over. If a candidate says "just pass it with `--build-arg` at runtime", that is a clear signal the two phases have not been separated in their head. ## Quick mental test Ask: "does the running process need to read this?" If yes, it must end up as `ENV` (or be passed at run time). "Does only the build need it?" Then `ARG`, and keep it out of the final image.

  • How would you make a value supplied at build time readable by the application at runtime?
    Declare it as `ARG` and immediately assign it to an `ENV` of the same or another name: `ARG APP_VERSION` followed by `ENV APP_VERSION=${APP_VERSION}`. The build arg supplies the value; the ENV persists it into the image config so the container process sees it. Do this only for non-sensitive values, since ENV is visible to anyone who can inspect the image.
  • Can a build argument be overridden when starting a container?
    No. Build args exist only while `docker build` runs and are not stored as environment in the image, so there is nothing for `docker run` to override. Changing a build arg requires rebuilding the image. Only `ENV` values can be overridden per-container with `-e`, `--env-file`, or a Compose `environment:` entry.
  • If a Dockerfile has both `ARG VERSION=1` and later `ENV VERSION=2`, what does a subsequent `RUN echo $VERSION` print?
    It prints 2. When an ENV and an ARG share a name, the ENV value takes precedence for the remainder of the build, regardless of what was passed with `--build-arg`. This is a documented rule and a common source of confusion when someone tries to override a value that the Dockerfile has already pinned with ENV.

ARG is scaffolding around a building — essential while constructing, taken away before anyone moves in. ENV is the wiring left inside the walls.

saying these in an interview costs you the question

  • Thinking a build arg is available as an environment variable inside the running container.
  • Trying to pass `--build-arg` to `docker run`, or `-e` to `docker build`, confusing the two phases.
  • Treating ARG as a safe place for tokens because 'it does not persist'.
  • Forgetting that an ARG must be re-declared in each build stage that uses it.
  • Assuming `--build-arg` always wins when an ENV of the same name is defined later in the Dockerfile.

context

open as a page

An image is built from a Dockerfile containing `ENV LOG_LEVEL=info`. What value does the process see when the container is started with `docker run -e LOG_LEVEL=debug`, and what happens if the Dockerfile also declares `ARG LOG_LEVEL` and the build is run with `--build-arg LOG_LEVEL=trace`?

level: middleimportance: must knowfreq 50%

basics

~20 s

The container sees debug: docker run -e overrides the image's ENV default. During the build, an ENV always beats a same-named ARG, so --build-arg LOG_LEVEL=trace is ignored for later build steps unless the ENV is written as ENV LOG_LEVEL=${LOG_LEVEL}.

open as a page

A colleague passes a private registry token to an image build with `docker build --build-arg NPM_TOKEN=...` and argues it is safe because build arguments do not persist into the running container. Is the token recoverable from the resulting image, and what should be used instead?

level: seniorimportance: must knowfreq 45%

basics

~20 s

Yes, it is recoverable. docker history --no-trunc shows the build instructions with the substituted value, and anything the token was written into (an .npmrc, a cached file) stays in that layer. Use a BuildKit secret mount instead, and rotate the token.

open as a page

A Dockerfile declares `ARG APP_VERSION=1.0` on the very first line, before any FROM instruction, and a later `RUN echo $APP_VERSION` inside the build stage prints nothing. Why is it empty, and how do you fix it?

level: middleimportance: should knowfreq 45%

basics

~20 s

An ARG declared before the first FROM lives in a global scope usable only by FROM lines. Build stages do not inherit it. Re-declare ARG APP_VERSION (no value needed) inside the stage; the value passed or defaulted globally is then applied.

open as a page

Why is the line `ENV DEBIAN_FRONTEND=noninteractive` in a Dockerfile considered a defect, and what are the correct ways to set a variable that only the package-installation step during the build should see?

level: seniorimportance: should knowfreq 35%

basics

~20 s

ENV persists into the image, so every container and every downstream image inherits DEBIAN_FRONTEND=noninteractive and any interactive apt or dpkg run later misbehaves. Set it per instruction (RUN DEBIAN_FRONTEND=noninteractive apt-get ...) or as an ARG, which does not persist.

open as a page