Dockerfile Fundamentals: FROM, RUN, COPY, ADD, WORKDIR, ENV, ARG, USER, CMD, and ENTRYPOINT: Diagnostics, Failure Modes, Security, and Performance
A Dockerfile can build successfully and still embed secrets, depend on a moving base, launch the wrong process, run unnecessarily as root, or destroy cache efficiency. This lesson diagnoses those failures from build output and image evidence before applying the smallest corrective change.
Learning objectives
- Preserve build output, Dockerfile text, base identity, image ID, history, and inspect evidence before rebuilding.
- Diagnose secret leakage, mutable base references, JSON/shell-form mistakes, non-root failures, and cache-hostile instruction order.
- Use Docker build checks as evidence without confusing lint success with a secure or reproducible image.
- Explain why deleting a sensitive file in a later layer does not erase it from an earlier layer.
- Apply the smallest source-controlled correction and prove the resulting image/config change independently.
# syntax=docker/dockerfile:1 for most users
who want the latest stable Dockerfile 1.x frontend, documents
ARG/ENV as inappropriate secret channels,
and supports docker build --check for Dockerfile build
checks. The labs resolve the base tag to a repository digest before
building and never require a registry push, paid service, privileged
mode, Docker socket mount, or broad prune.
1. Diagnose the build as an evidence chain
Before changing the Dockerfile, preserve the exact file, builder versions, base reference/digest, build command, build output, image ID, history, and Config fields. If a container fails, also preserve its ID, image ID, command, user, exit code, logs, and runtime overrides. Rebuilding first can replace the evidence needed to explain the failure.
docker context show
docker version
docker buildx version
sha256sum Dockerfile
docker image inspect IMAGE --format '{{.Id}} {{json .Config}}'
docker history --no-trunc IMAGE
2. Failure: secret-like data in ARG, ENV, or copied layers
Use only the synthetic value below. It is intentionally fake.
# BAD DEMO — fake value only
FROM busybox:1.37.0
ARG DEMO_TOKEN=not-a-secret
ENV API_TOKEN=not-a-secret
RUN printf '%s\n' "$DEMO_TOKEN" > /tmp/token.txt
RUN rm -f /tmp/token.txt
CMD ["true"]
Current Docker build checks can flag secret-like ARG/ENV keys. More importantly, removing the file in a later layer does not erase the earlier filesystem content from the image's layer history. The correct repair is architectural: never provide the real secret through ARG/ENV/COPY; use a secret mount only for the RUN instruction that needs it, and ensure the command does not persist or print it.
# Correct pattern when a build command truly needs a secret
RUN --mount=type=secret,id=api_token \
some-tool --token-file /run/secrets/api_token
3. Failure: a mutable base reference hides input drift
FROM some-image:latest can select different content on
two dates. A green rebuild therefore does not prove byte-for-byte
source equivalence. Preserve the human tag, resolve and record the
digest, and decide explicitly how updates are reviewed. If the image
is rebuilt to adopt a patched base, that should create a new
artifact identity—not be described as “the same image.”
4. Failure: incorrect CMD/ENTRYPOINT quoting and shell assumptions
Consider a JSON-form command that expects shell expansion:
ENV APP_MODE=prod
CMD ["echo $APP_MODE"]
Exec/JSON form does not invoke a shell to expand
$APP_MODE; Docker tries to execute a program literally
named echo $APP_MODE. If shell expansion is required,
invoke a shell explicitly. If the application can read the
environment itself, the stronger pattern is to execute the
application directly and let it consume APP_MODE.
Build checks also flag shell-form ENTRYPOINT/CMD patterns because of signal/process implications. A lint warning is a starting hypothesis, not permission to mechanically rewrite commands without understanding behavior.
5. Failure: final process runs as root without need
If
docker image inspect IMAGE --format '{{.Config.User}}'
is empty, Docker normally uses the image's default, commonly root.
Do not “fix” the problem with host permission broadening such as
chmod 777. Determine which application paths require
reads/writes, set ownership during the build, switch USER, and run a
smoke test that exercises those paths.
6. Failure: cache-hostile instruction order
A common pattern copies the entire source tree before installing
rarely changing dependencies. Every source edit then invalidates the
dependency layer. Preserve --progress=plain output,
identify where cache reuse stopped, and reorder inputs only when the
application's dependency model supports it. Do not chase cache hits
by hiding inputs or using stale package metadata.
7. Failure: package update and install split across cache boundaries
On package managers where metadata update and installation must
remain coherent, putting apt-get update in one cached
RUN and apt-get install in a later RUN can reuse stale
metadata. The safer pattern is a single well-scoped RUN that
updates, installs exact intended packages, and removes
package-manager cache files when appropriate. The exact command is
distro-specific; do not paste Debian guidance into Alpine or vice
versa.
8. Controlled broken example: detect, preserve, repair
Create this synthetic Dockerfile in a disposable directory:
# syntax=docker/dockerfile:1
FROM busybox:1.37.0
ENV DEMO_PASSWORD=not-a-secret
WORKDIR relative/path
CMD echo "mode=$DEMO_PASSWORD"
docker build --check -f Dockerfile.broken . 2>&1 | tee broken-check.txt || true
Expected current checks can include secret-like ENV, relative WORKDIR, and JSON-argument guidance. Keep the raw output. Then repair the model rather than merely silencing rules: remove the fake credential pattern entirely, use an absolute workdir, and choose an exec-form application command. Re-run the check and preserve both before/after outputs.
9. Why “delete it later” is not erasure
Image layers are content-addressed build results. If one layer adds a sensitive file and a later layer deletes that pathname, the final merged filesystem may hide the file, but the earlier layer blob can still contain the data. The only safe fix for a real leak is to treat the secret as exposed, rotate it, remove it from build inputs/history, and rebuild clean artifacts from a history that never included the secret.
10. Performance diagnosis without cargo-cult optimization
Measure context transfer, build duration, cache hits/misses, resulting image size, and frequently invalidated steps. A smaller number of Dockerfile lines is not automatically faster. A smaller image is not automatically more secure. Optimize measured bottlenecks while preserving readable ownership of build inputs and runtime configuration.
11. Evidence-first diagnostic sequence
- Capture context, Engine/CLI/Buildx state and the exact Dockerfile/source hashes.
- Capture the base tag plus resolved digest/platform.
- Run
docker build --checkand preserve output. - Run the build with plain progress and preserve the first failure.
- If an image exists, inspect ID, Config, history, and relevant filesystem evidence.
- If a container exists, inspect command/user/exit/log state separately.
- Change one source hypothesis.
- Rebuild and compare identities/evidence; do not delete the original evidence packet.
Knowledge check
A fake token copied into one layer and deleted in the next is
absent from docker run. Is the image history
clean?
No. Earlier layer content may still contain the file. Real leaked credentials must be rotated and the artifact rebuilt from clean history.
Why can CMD ["echo $APP_MODE"] fail?
JSON/exec form does not perform shell expansion; it treats the array element as the executable/argument exactly as written.
What should you inspect before changing permissions to fix a non-root failure?
The configured/effective UID/GID and the ownership/mode of only the paths the application must access. Avoid broad host permission weakening.
What does a cache miss prove?
Only that BuildKit could not reuse the cached result for that step under current inputs; it does not identify the cause until you inspect the relevant instruction and inputs.
Should you delete first-failure build logs after a successful repair?
No. Preserve before/after evidence so the causal change is auditable and repeatable.
Official references and version notes
- Dockerfile reference — current syntax and semantics for FROM, RUN, COPY, ADD, WORKDIR, ENV, ARG, USER, CMD, ENTRYPOINT, parser directives, and instruction options.
- Building best practices — guidance on reusable images, cache-aware instruction order, package installation, pinning, and non-root design.
- Build variables — lifecycle and scope of ARG and ENV.
- Build secrets — secret and SSH mounts for sensitive build-time input.
- Build checks reference — Dockerfile checks including SecretsUsedInArgOrEnv, JSONArgsRecommended, and WorkdirRelativePath.
-
Checking build configuration
— running checks with
docker build --checkand interpreting results. - Docker Engine 29 release notes — current Engine 29 behavior and packaging updates.
- BuildKit releases — current builder/frontend changes that can affect Dockerfile features.
Version-sensitive statements were rechecked against primary
documentation on 2026-09-21. Docker Engine 29.8.1
is the current Engine 29 patch release at this checkpoint. Docker
currently recommends # syntax=docker/dockerfile:1 for
most users when an external stable Dockerfile frontend is desired;
that reference follows the latest stable 1.x frontend rather than
being an immutable artifact. Executable labs therefore record the
learner's actual Engine/CLI/Buildx/BuildKit state and resolve the
application base image to a repository digest before building.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.