Writing Dockerfiles
The Dockerfile is the source code of your image. Learn every major instruction, cache behavior, and the patterns that make builds fast, small and secure.
What is this?
A Dockerfile is a plain-text recipe executed top-to-bottom to assemble an image. Each instruction becomes a layer. The two most subtle distinctions are COPY vs ADD and CMD vs ENTRYPOINT — get those right and most Dockerfile mistakes disappear.
Why do you need it?
- It turns infrastructure into reviewable, versioned, auditable source code.
- One file defines the entire runtime environment, eliminating drift between environments.
- Follows the same engineering practices as application code: review, test, lint.
Mental model
A recipe card
A Dockerfile
The recipe lists ingredients (base image) and steps (instructions). Every cook produces the same dish, just as every build produces the same image.
How it works
- FROM chooses the base image; every layer after it builds on it.
- COPY/ADD inject files from the build context into the image.
- RUN executes commands in a new layer on top of the current filesystem.
- ENV/ARG set environment and build-time variables.
- CMD sets the default command; ENTRYPOINT sets the immutable entrypoint; together they let you pass args.
- Multi-stage builds copy artifacts between stages with COPY --from.
Instruction → layer mapping in a Dockerfile.
Key commands
docker build -t app .Build from the Dockerfile in the context.
docker build --target builder -t app-build .Build only a named stage (useful for debugging).
docker build --build-arg VERSION=1.0 -t app .Pass a build argument into ARG instructions.
docker build --platform linux/amd64 -t app .Build for a different architecture.
docker buildx build --push --platform linux/amd64,linux/arm64 -t user/app:1.0 .Build and push multi-architecture images.
Examples
Multi-stage Go build
FROM golang:1.22 AS builderWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o /out/server . FROM scratchCOPY --from=builder /out/server /serverCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ENTRYPOINT ["/server"]EXPOSE 8080The builder stage compiles a static Go binary; the final stage copies only that binary and TLS certs into an empty scratch image — result can be under 10MB.
In production
- Lint Dockerfiles in CI (hadolint) and scan images (Trivy) before promotion.
- Distroless/scratch final stages shrink attack surface to just the app binary.
- Multi-arch builds via buildx so one tag works on amd64 and arm64.
- Signed images and a pinned base digest for supply-chain integrity.
Best practices
- Prefer COPY over ADD; it's unambiguous.
- Use ENTRYPOINT for the binary and CMD for default flags/args.
- Consolidate RUN commands with && and clean up package managers in the same layer.
- Copy dependency manifests before source to keep the expensive layer cached.
- Set USER to a non-root account and drop unnecessary capabilities.
- Add a HEALTHCHECK so orchestrators and load balancers know real readiness.
Common mistakes
- ADD a remote tarball/URL instead of curl in a RUN (unresolvable, unverifiable, uncacheable).
- Putting credentials in ARG or ENV (they remain in image metadata and layers).
- Using apt-get without apt-get clean in the same RUN, bloating the layer.
- Running everything as root.
Common issues
RUN apt-get update && apt-get install ... returns error 100Package lists unavailable or base image lacks the package manager index.
Chain apt-get update && apt-get install -y in one RUN; for alpine use apk add --no-cache.
COPY --from=builder: no stage or image foundStage name mismatch or --target used on a build that needs that stage.
Verify the stage alias in FROM matches the --from reference.
Multiple platforms feature is currently not supportedLegacy builder used instead of buildx.
Enable BuildKit (DOCKER_BUILDKIT=1 or default in modern Docker) and use docker buildx.
Debugging tips
- docker build --progress=plain to see every command's output.
- docker build --no-cache to bypass stale cache.
- Insert a temporary `RUN pwd && ls -la` or use `docker run --entrypoint sh IMAGE` to explore.
- Use a linter: hadolint catches 95% of footguns automatically.
Interview checkpoint
Hands-on lab
- Write a multi-stage Dockerfile
- Cut final image size dramatically
- Use build args and correct CMD/ENTRYPOINT
- Lint the Dockerfile
Multi-stage Dockerfile Mastery
40 min · intermediate
Quick quiz
Which instruction is best for copying a directory into the image?
In a multi-stage build, how do you copy a file from an earlier stage?
Secrets passed as ENV in a Dockerfile are visible in the final image.
Cheat sheet
FROM base:tagBase imageCOPY src dstCopy files from contextRUN commandExecute and record layerENV K=VRuntime env varARG K=VBuild-time variableCMD ["cmd","arg"]Default argsENTRYPOINT ["bin"]Fixed commandEXPOSE portDocument port (does not publish)