Chapter 07Lesson 02~115 minutes

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.

Hands-onFROMCOPY/RUNUSERENTRYPOINT/CMD

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.
Chapter 07 evidence baseline — verified 2026-09-21. Mandatory exercises use synthetic source, local disposable images/containers, exact chapter labels/names, and no real credentials. Docker Engine 29.8.1 is the current Engine 29 patch baseline at verification time, but each lab records the learner's actual Engine/CLI/Buildx/BuildKit state. Current Docker documentation recommends # 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.

Next lesson

Next: Configuration, Design Choices, and Tradeoffs

Turn the mechanics into repeatable decisions about copying, variables, process forms, users, and cache-aware instruction design.

Knowledge check

Why does the lab resolve busybox:1.37.0 to a repository digest before building?

Why can the local final image have no RepoDigest?

What does docker run IMAGE learner change in this image?

Does a passing docker build --check prove there are no vulnerabilities?

What ADD behavior did the lab demonstrate?

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.