Dockerfile Fundamentals: FROM, RUN, COPY, ADD, WORKDIR, ENV, ARG, USER, CMD, and ENTRYPOINT: Guided Hands-On Workflow and Core Operations
Build a small non-root image step by step and prove what each Dockerfile instruction changes. The workflow resolves a versioned base tag to an immutable repository digest, uses BuildKit checks, inspects image configuration and history, and verifies runtime identity without introducing real credentials or production dependencies.
Learning objectives
- Resolve a versioned base tag to a repository digest and use that digest as the build input.
- Build a tiny image progressively with WORKDIR, COPY, RUN, ARG, ENV, USER, ENTRYPOINT, and CMD.
- Run Dockerfile build checks and capture plain BuildKit progress before producing the image.
- Inspect Config.User, WorkingDir, Env, Entrypoint, Cmd, history, and final image ID/digest evidence.
- Run the image as a non-root user and demonstrate intentional ENTRYPOINT/CMD override behavior.
# 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. Lab scope: one tiny image, one declared behavior
The lab builds a tiny BusyBox-based image that prints a greeting, runtime mode, effective UID, and working directory. The source is synthetic, the base tag is resolved to an immutable repository digest before the build, and all images/containers are labeled or named for this chapter. No registry push, production endpoint, secret, privileged container, or Docker socket mount is required.
2. Preflight: record the actual client, daemon, context, builder, and base identity
mkdir -p ch07-lab/evidence
cd ch07-lab
docker context show | tee evidence/00-context.txt
docker version | tee evidence/01-version.txt
docker info | tee evidence/02-info.txt
docker buildx version | tee evidence/03-buildx.txt
BASE_TAG='busybox:1.37.0'
docker pull "$BASE_TAG"
BASE_IMAGE=$(docker image inspect "$BASE_TAG" --format '{{index .RepoDigests 0}}')
printf 'base_tag=%s\nbase_digest_ref=%s\n' "$BASE_TAG" "$BASE_IMAGE" | tee evidence/04-base.txt
The exact repository digest can vary by registry/platform resolution
over time. That is why the lab captures it rather than hard-coding a
digest in the lesson text. The build later receives
$BASE_IMAGE as a non-secret argument.
3. Create the smallest application source
cat > app.sh <<'EOF'
#!/bin/sh
set -eu
name="${1:-academy}"
printf 'hello=%s mode=%s uid=%s cwd=%s\n' \
"$name" "${APP_MODE:-unset}" "$(id -u)" "$PWD"
printf 'message='; cat ./message.txt
EOF
chmod 0755 app.sh
printf 'declared-by-dockerfile\n' > message.txt
sha256sum app.sh message.txt | tee evidence/05-source-hashes.txt
These two files are the only application inputs we intend to copy.
Chapter 08 will make the context boundary and
.dockerignore rules explicit.
4. Write a Dockerfile with deliberate phase boundaries
# syntax=docker/dockerfile:1
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
ARG BUILD_LABEL=local
ENV APP_MODE=learning
WORKDIR /app
COPY --chmod=0555 app.sh ./app.sh
COPY --chmod=0444 message.txt ./message.txt
RUN printf 'build-label=%s\n' "$BUILD_LABEL" > /app/build-label.txt
USER 65532:65532
ENTRYPOINT ["/app/app.sh"]
CMD ["academy"]
The global ARG BASE_IMAGE exists so FROM can use the
digest reference. BUILD_LABEL is a deliberately
non-secret build parameter whose effect is persisted into a
file. APP_MODE is intentionally runtime configuration.
The image changes to numeric UID/GID 65532 before defining its
runtime command.
5. Run build checks before spending time on the build
docker build --check \
--build-arg BASE_IMAGE="$BASE_IMAGE" \
--build-arg BUILD_LABEL=ch07-lab \
. | tee evidence/06-build-check.txt
A clean check is useful evidence, not a security certificate. Build checks analyze known Dockerfile patterns; they do not prove the base image is vulnerability-free, the application is correct, or the runtime environment is safe.
6. Build with plain progress and capture local image identity
IMAGE='devops-academy-ch07:fundamentals'
docker build --progress=plain \
--build-arg BASE_IMAGE="$BASE_IMAGE" \
--build-arg BUILD_LABEL=ch07-lab \
--label devops-academy.lab=ch07 \
-t "$IMAGE" . 2>&1 | tee evidence/07-build.txt
docker image inspect "$IMAGE" --format 'ImageID={{.Id}} RepoTags={{json .RepoTags}} RepoDigests={{json .RepoDigests}}' \
| tee evidence/08-image-identity.txt
A locally built image may have no repository digest until it is pushed or otherwise associated with registry content. That is expected. Do not invent a registry digest; record the local image ID/config identity and the exact base digest separately.
7. Inspect the image configuration, not just the build log
docker image inspect "$IMAGE" --format '{{json .Config}}' | tee evidence/09-config.json
docker image inspect "$IMAGE" --format 'User={{.Config.User}} Workdir={{.Config.WorkingDir}} Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' \
| tee evidence/10-runtime-defaults.txt
docker history --no-trunc "$IMAGE" | tee evidence/11-history.txt
Expected defaults are User=65532:65532,
Workdir=/app, ENTRYPOINT ["/app/app.sh"],
and CMD ["academy"]. History is useful, but it is not a
complete provenance system; later chapters add stronger supply-chain
evidence.
8. Run the default command and verify non-root behavior
NAME='devops-academy-ch07-default'
docker run --name "$NAME" --label devops-academy.lab=ch07 "$IMAGE" \
| tee evidence/12-default-run.txt
docker inspect "$NAME" --format 'ID={{.Id}} Image={{.Image}} User={{.Config.User}} Path={{.Path}} Args={{json .Args}} Exit={{.State.ExitCode}}' \
| tee evidence/13-default-inspect.txt
The output should report UID 65532, working directory
/app, runtime mode learning, and the
declared message. The container exits because this tiny program is a
batch-style process.
9. Override CMD arguments without replacing ENTRYPOINT
docker run --rm --label devops-academy.lab=ch07 "$IMAGE" learner \
| tee evidence/14-cmd-override.txt
docker run --rm --label devops-academy.lab=ch07 -e APP_MODE=verification "$IMAGE" reviewer \
| tee evidence/15-env-override.txt
In both cases, /app/app.sh remains the ENTRYPOINT. The
word after the image name replaces the default CMD argument. The
second command also overrides the image's ENV default at container
creation. Image configuration remains unchanged.
10. Prove ARG is not automatically a runtime variable
docker run --rm --entrypoint sh "$IMAGE" -c \
'printf "BUILD_LABEL=%s\n" "${BUILD_LABEL-unset}"; cat /app/build-label.txt' \
| tee evidence/16-arg-proof.txt
BUILD_LABEL should be unset as a runtime environment
variable, while /app/build-label.txt contains the
build-time value because the RUN instruction intentionally persisted
its effect. This is the correct lifecycle distinction—and also why a
secret passed as ARG is unsafe.
11. COPY versus ADD: demonstrate the special behavior safely
Create a local tar archive and a one-off Dockerfile that uses ADD to unpack it. This is synthetic and local; it does not download remote content.
mkdir -p add-demo
printf 'inside-local-tar\n' > add-demo/item.txt
tar -czf bundle.tar.gz -C add-demo item.txt
cat > Dockerfile.add <<'EOF'
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
WORKDIR /demo
ADD bundle.tar.gz /demo/
CMD ["cat", "/demo/item.txt"]
EOF
docker build --build-arg BASE_IMAGE="$BASE_IMAGE" -f Dockerfile.add -t devops-academy-ch07:add-demo .
docker run --rm devops-academy-ch07:add-demo | tee evidence/17-add-demo.txt
The extraction is ADD-specific behavior. If the requirement were merely “copy this tar file as a tar file,” use COPY. Choosing ADD should communicate that its extra source semantics are intentional.
12. Challenge: change one lifecycle property deliberately
Choose exactly one change—different runtime ENV default, different CMD argument, or different non-secret BUILD_LABEL. Predict whether the final image ID/config or only container state should change, perform the smallest rebuild/run, and capture evidence that proves your prediction.
13. Guarded cleanup
docker ps -a --filter label=devops-academy.lab=ch07 --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}'
docker rm "$NAME"
docker image rm devops-academy-ch07:add-demo "$IMAGE"
docker ps -a --filter label=devops-academy.lab=ch07
Do not use broad prune. If image removal is blocked, inspect the exact dependent container/image reference instead of escalating destructively.
Knowledge check
Why does the lab resolve busybox:1.37.0 to a
repository digest before building?
The tag is a readable reference, while the resolved digest identifies the exact registry content selected for the build.
Why can the local final image have no RepoDigest?
A local build can have an image ID and tag without having been associated with registry content. Do not invent a registry digest.
What does docker run IMAGE learner change in this
image?
It replaces the default CMD argument while preserving the configured ENTRYPOINT.
Does a passing docker build --check prove there
are no vulnerabilities?
No. Build checks detect selected configuration anti-patterns; vulnerability, provenance, signature, runtime, and application validation are separate evidence.
What ADD behavior did the lab demonstrate?
Automatic extraction of a local tar archive into the image filesystem.
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.