A container image is a packaged filesystem plus metadata that says how to start your application. A Dockerfile is the recipe for building it: a text file of instructions that the builder runs top to bottom. The CKAD exam expects you to read, fix and write short Dockerfiles, so you need to know what each common instruction does and how the result is stored.
Every Dockerfile starts from a base image with FROM, for example FROM python:3.12-slim or FROM alpine. The base supplies the operating system files and often a language runtime. Smaller bases such as slim, alpine or distroless images mean faster pulls and fewer packages that could contain vulnerabilities. After FROM you typically use WORKDIR to set the working directory, COPY to bring in source files, RUN to execute build commands such as installing dependencies, ENV to set environment variables, EXPOSE to document a listening port and USER to switch away from root.
Instructions that change the filesystem (mainly RUN, COPY and ADD) each create a new read-only layer. Layers are cached: if an instruction and everything before it are unchanged, the builder reuses the cached layer instead of running it again. That is why you copy the dependency manifest (such as requirements.txt or package.json) and install dependencies before copying the rest of your source code. Editing a source file then only invalidates the later layers, and rebuilds are fast. Deleting a file in a later layer does not shrink the image, because the earlier layer still contains it.
A multi-stage build uses more than one FROM in the same file. The first stage has compilers and build tools; the final stage starts from a small runtime image and copies only the finished artifact with COPY --from=build. Build tools never reach the image you ship, which makes it smaller and reduces its attack surface.
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app
FROM alpine
COPY --from=build /app /app
USER 1000
ENTRYPOINT ["/app"]
CMD ["--port", "8080"]
ENTRYPOINT and CMD together define the start command. ENTRYPOINT is the executable that always runs; CMD supplies default arguments that are easy to override. When both are present, the container runs ENTRYPOINT followed by CMD, so the example above runs /app --port 8080. If you run the image with extra arguments, they replace CMD but keep ENTRYPOINT. If only CMD is set, it is the whole command and any arguments you pass replace it entirely.
Prefer the exec form, a JSON array like ["/app"], over the shell form ENTRYPOINT /app. The exec form runs your program directly as process 1, so it receives signals such as SIGTERM when Kubernetes stops the Pod. The shell form wraps it in /bin/sh -c, and the shell may not forward signals, leading to slow, forced shutdowns.
Key terms
- Base image
- The image named in FROM that supplies the starting filesystem and runtime for your build.
- Layer
- A cached, read-only filesystem change produced by an instruction such as RUN or COPY.
- Multi-stage build
- A Dockerfile with several FROM stages where only selected artifacts are copied into the final image.
- ENTRYPOINT
- The fixed executable the container runs at start.
- CMD
- Default arguments (or a default command if there is no ENTRYPOINT) that are replaced by arguments given at run time.
A team's Python image took four minutes to rebuild after every code change. They moved COPY requirements.txt . and RUN pip install -r requirements.txt above COPY . ., so the dependency layer stayed cached, and rebuilds dropped to seconds. Switching to a multi-stage build also cut the final image size by more than half.
Check yourself
Why should you copy the dependency file and install packages before copying the rest of the source?
Because layers are cached in order; code changes then invalidate only the later layers, so the slow dependency install is reused from cache.
An image has ENTRYPOINT ["ping"] and CMD ["localhost"]. What runs if you start it with the argument example.com?
ping example.com, because run-time arguments replace CMD while ENTRYPOINT stays.
What does a multi-stage build achieve?
It keeps build tools and intermediate files out of the final image, making it smaller and reducing attack surface.