skip to content

Dockerfile & Images

The build recipe: the instruction set from FROM through ENTRYPOINT, multi-stage builds, the build context, and the BuildKit engine that runs them. A Dockerfile is short enough to read in an interview and instantly shows how you think about size and root.

part ofDockeroverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

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

You are picking the base image named in a Dockerfile's FROM instruction for a backend service. Compare a full distribution image (debian/ubuntu), a -slim variant, Alpine, distroless images, and scratch, and explain how you would choose between them.

level: juniorimportance: must knowfreq 70%

basics

~20 s

FROM sets the filesystem your image starts from. Full distro images have every tool but are large; -slim strips docs and extras; Alpine is tiny but uses musl libc; distroless ships only the runtime with no shell or package manager; scratch is empty and only fits static binaries. Pick the smallest image that still runs and that you can still debug.

open as a page

Docker's BuildKit builder replaced the classic image builder as the default. What does BuildKit do differently, and what does that change about how you write a Dockerfile?

level: juniorimportance: must knowfreq 48%

basics

~20 s

The classic builder ran instructions strictly top to bottom, one container per step. BuildKit builds a dependency graph, so independent stages run in parallel, unused stages are skipped, and only the build-context files actually needed are transferred. It also adds features the old builder lacks: secret, cache and ssh mounts, and multi-platform builds.

open as a page

In a Dockerfile, what is the difference between the CMD and ENTRYPOINT instructions, and what happens to each of them when you pass extra arguments after the image name in `docker run`?

level: juniorimportance: must knowfreq 78%

basics

~20 s

ENTRYPOINT is the executable that always runs; CMD gives default arguments, or the whole command when there is no ENTRYPOINT. Arguments after the image name in docker run replace CMD but are appended to ENTRYPOINT.

open as a page

When you run `docker build -t app .`, what does the trailing `.` mean, and what actually gets sent to the builder before the first instruction executes?

level: juniorimportance: must knowfreq 58%

basics

~20 s

The . is the build context: the directory tree the CLI packages up and hands to the builder before building. Only files inside it can be used by COPY, and everything not excluded by .dockerignore is transferred.

open as a page

A Dockerfile that compiles an application and then ships it produces a 900 MB image containing the compiler, package-manager caches and the full source tree. How does putting more than one FROM instruction in a single Dockerfile fix that, and what exactly ends up in the final image?

level: juniorimportance: must knowfreq 80%

basics

~20 s

Each FROM starts a new build stage with its own fresh filesystem. Compile in a toolchain stage, then start a slim runtime stage and pull across only the built artifact with COPY --from. Only the last stage's layers ship.

open as a page

In a Dockerfile, what is the difference between the COPY and ADD instructions, and why do most style guides say to default to COPY?

level: juniorimportance: must knowfreq 75%

basics

~20 s

COPY just copies files from the build context into the image. ADD does that plus two magic behaviours: it auto-extracts local tar archives and can fetch remote URLs. Default to COPY because its behaviour is predictable; use ADD only when you want extraction.

open as a page

What does the USER instruction in a Dockerfile do, and what else must change in the image when you add it before the final CMD?

level: juniorimportance: must knowfreq 76%

basics

~20 s

USER sets the UID/GID that later build steps and the container's main process run as. Without it everything runs as root (UID 0). Recipe: create a user, chown the app files to it, then USER before CMD.

open as a page

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

level: juniorimportance: must knowfreq 64%

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.

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

In a Dockerfile's FROM instruction, what is the difference between referencing a base image by tag (for example ubuntu:24.04) and by digest (image@sha256:...), and when does each matter?

level: middleimportance: must knowfreq 60%

basics

~20 s

A tag is a mutable pointer: the same tag can resolve to different image content over time. A digest is the sha256 hash of the image manifest, so it is immutable and content-addressed. Tags give you automatic patch updates; digests give you reproducible, verifiable builds. Pin digests for release builds and update them with automation.

open as a page

Why does BuildKit never run a Dockerfile stage that nothing references, when the legacy builder ran every stage?

level: middleimportance: must knowfreq 62%

basics

~20 s

BuildKit converts the Dockerfile into a dependency graph and solves only the steps the requested output actually needs, so a stage nothing copies from is pruned. The legacy builder walked the file top to bottom, so every stage ran.

open as a page

During a Docker image build you need a private credential, for example a package registry token or an SSH key for a private Git dependency. How do you supply it so it never ends up in the published image?

level: middleimportance: must knowfreq 55%

basics

~20 s

Use BuildKit mounts. RUN --mount=type=secret,id=tok exposes the value as a file under /run/secrets for that one instruction only, and it is never written to a layer or to image history. For private Git over SSH use --mount=type=ssh to forward the agent. Never pass credentials via ARG, ENV or COPY.

open as a page

A Dockerfile ends with `CMD python app.py` instead of `CMD ["python", "app.py"]`. What is the practical difference between those two forms, and why does it matter when someone runs `docker stop` on the container?

level: middleimportance: must knowfreq 64%

basics

~20 s

Shell form runs the command via /bin/sh -c, so the shell is PID 1 and usually does not forward the SIGTERM that docker stop sends — the container is SIGKILLed after the timeout. Exec (JSON) form runs the process directly as PID 1, so it receives the signal.

open as a page

How does a `.dockerignore` file work — where must it live, what pattern syntax does it use, and in what ways does its matching differ from `.gitignore`?

level: middleimportance: must knowfreq 52%

basics

~20 s

It sits at the build-context root and lists patterns excluded before the context is sent. Patterns are path-matched with *, ? and **, ! re-includes, and the last matching line wins. Unlike .gitignore, a bare name like node_modules matches only at the top level — use **/node_modules.

open as a page

Why does docker buildx --load reject a multi-platform build, and what do you use instead?

level: middleimportance: must knowfreq 58%

basics

~20 s

The --load flag copies the build result into the local Docker image store, which holds one architecture per tag, so a two-platform result has nowhere to go. Push it to a registry with --push instead, or load one platform at a time.

open as a page

Why do Dockerfiles so often chain many shell commands into a single RUN instruction with `&&`, and what goes wrong if you split them into separate RUN lines and clean up afterwards?

level: middleimportance: must knowfreq 65%

basics

~20 s

Every RUN, COPY and ADD commits a new immutable layer. Files created in one layer and deleted in a later one still ship — deletion only writes a whiteout marker. Chaining with && lets the cleanup happen inside the same layer, so the bytes never exist in the image.

open as a page

What does the EXPOSE instruction in a Dockerfile actually do at runtime, and how do you make a container's port reachable from the host?

level: middleimportance: must knowfreq 58%

basics

~20 s

EXPOSE only records metadata: which ports the image intends to listen on. It publishes nothing. To reach a port from the host you must publish it at run time with -p host:container, or -P to map every EXPOSEd port to a random host port.

open as a page

How does the Dockerfile HEALTHCHECK instruction work, and what do its --interval, --timeout, --retries and --start-period options control?

level: middleimportance: must knowfreq 52%

basics

~20 s

HEALTHCHECK CMD runs a command inside the container on a schedule: exit 0 means healthy, 1 means unhealthy. --interval is the gap between checks, --timeout kills a hung check, --retries is how many consecutive failures flip the status, --start-period is a startup grace window where failures do not count.

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

What does a BuildKit cache mount (RUN --mount=type=cache) do, how does it differ from Docker's normal layer caching, and when would you reach for it?

level: middleimportance: should knowfreq 45%

basics

~20 s

A cache mount gives a RUN instruction a persistent directory that lives on the builder across builds and is not part of any layer. Layer caching reuses whole instruction results and is invalidated by any input change; a cache mount still helps after invalidation, because the package manager's downloads or compiler outputs survive. Use it for dependency and compiler caches.

open as a page

An image's configured start-up command crashes immediately, so the container never stays up long enough to inspect. How do you get a shell inside that image using `docker run`, and what exactly does the `--entrypoint` flag change?

level: middleimportance: should knowfreq 46%

basics

~10 s

Run docker run --rm -it --entrypoint sh myimage. The flag replaces the image's ENTRYPOINT for that container only; anything you type after the image name still becomes the arguments, replacing CMD.

open as a page

Your Dockerfile lives at `docker/api.Dockerfile` but the build must COPY files from the repository root. How do you invoke `docker build` for that, and what context sources other than a local directory does `docker build` accept?

level: middleimportance: should knowfreq 34%

basics

~20 s

Run docker build -f docker/api.Dockerfile -t api . from the repository root: -f locates the Dockerfile, the positional argument sets the context. Besides a local directory, the context can be a Git repository URL, a tarball URL, or a tar/Dockerfile piped on stdin with -.

open as a page

In a Dockerfile, what do BUILDPLATFORM and TARGETARCH mean, and what does FROM --platform=$BUILDPLATFORM do?

level: middleimportance: should knowfreq 44%

basics

~20 s

BUILDPLATFORM describes the machine running the build; TARGETARCH describes the architecture currently being produced. Pinning a stage with FROM --platform=$BUILDPLATFORM keeps that stage on the builder's own architecture so its toolchain runs natively and cross-produces output for TARGETARCH.

open as a page

Besides a previously declared build stage, what else can the `--from` flag on a Dockerfile COPY instruction refer to, and what are the rules for naming and referencing stages?

level: middleimportance: should knowfreq 55%

basics

~20 s

COPY --from accepts a stage name declared with FROM ... AS name, a stage index (--from=0), or any external image reference such as --from=nginx:1.27. Source paths are resolved inside that stage or image, not in the build context.

open as a page

You need a development image containing test tooling and a lean production image, both from one Dockerfile. How would you structure the stages, and which `docker build` flag selects which image you get?

level: middleimportance: should knowfreq 50%

basics

~20 s

Layer the stages: a shared base, a deps stage, a dev/test stage with tooling, and a lean final stage. Build a specific one with docker build --target dev. Without --target the last stage in the file is built, so keep production last.

open as a page

The RUN instruction in a Dockerfile can be written as `RUN apt-get update` or as `RUN ["apt-get", "update"]`. What is the difference between these two forms, and when does the choice actually change the outcome?

level: middleimportance: should knowfreq 50%

basics

~20 s

The string form is shell form: Docker runs it via /bin/sh -c, so variables, globs, pipes and && work. The JSON-array form is exec form: the binary is executed directly with no shell, so shell syntax is literal and the image needs no shell.

open as a page

How do you attach metadata such as source repository, version and build revision to a container image, and which key names are standardized?

level: middleimportance: should knowfreq 38%

basics

~20 s

Use LABEL key=value in the Dockerfile; values land in the image config and are readable with docker inspect. Use the standard OCI keys org.opencontainers.image.* (source, revision, version, created, title, licenses) so tooling and registries recognize them. MAINTAINER is obsolete.

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

showing 1–30 of 47