Dockerfile Instructions & Multi-Stage Builds
A Dockerfile is a declarative, line-by-line recipe for building an image. Each instruction either creates a new layer (FROM, RUN, COPY, ADD) or sets image metadata that doesn't add a filesystem layer
A Dockerfile is a declarative, line-by-line recipe for building an image. Each instruction either creates a new layer (FROM, RUN, COPY, ADD) or sets image metadata that doesn't add a filesystem layer (ENV, ARG, EXPOSE, WORKDIR, CMD, ENTRYPOINT, HEALTHCHECK).
The twelve instructions covered here — FROM, RUN, COPY, ADD, WORKDIR, ENV, ARG, EXPOSE, CMD, ENTRYPOINT, HEALTHCHECK — cover essentially every Dockerfile you will see in a Java/Spring Boot shop. Multi-stage builds are the single highest-leverage technique for keeping production images small and secure.
The most interview-critical distinction is CMD vs ENTRYPOINT, and how they combine — this is asked in nearly every Docker interview at any seniority level.
-
FROM sets the base image and resets the build state; multiple FROM lines create multiple build stages, each with its own layer history, addressable by index (0, 1, 2...) or an alias (AS builder).
-
RUN executes a command in a new layer on top of the current state, committing the filesystem diff. Shell form (RUN cmd) runs via /bin/sh -c; exec form (RUN ["cmd","arg"]) runs the binary directly without a shell, avoiding shell-specific quirks (signal handling, variable expansion).
-
COPY copies files from the build context (or another build stage via --from) into the image filesystem, with no network or archive-extraction behavior — it's a pure, predictable copy.
-
ADD does everything COPY does, plus two extra (often surprising) behaviors: it can fetch from a remote URL, and it automatically extracts local tar/gzip archives into the destination directory.
-
WORKDIR sets (and creates, if missing) the working directory for all subsequent RUN/CMD/ENTRYPOINT/ COPY/ADD instructions — it persists across instructions, unlike a shell cd which would not.
-
ENV sets environment variables that are baked into the image and visible to every process started in any container from that image, as well as during the build itself for subsequent instructions.
-
ARG defines a build-time-only variable, available during docker build (e.g., for choosing a base image version) but NOT present in the final running container unless explicitly also assigned to an ENV.
-
EXPOSE is purely documentation/metadata — it does not actually publish a port. Without an explicit -p flag at docker run (or ports: in Compose), the container is not reachable from the host.
-
CMD provides default arguments for the container's main process; ENTRYPOINT defines the executable itself. If both are present, CMD's value becomes the argument list passed to ENTRYPOINT.
-
HEALTHCHECK defines a command Docker runs periodically inside the container to determine if the container is 'healthy', exposing this status via docker ps and to orchestrators for routing decisions.
-
Multi-stage builds let you use one stage (with a full SDK/build toolchain) to compile/build, then COPY --from=builder only the final artifact into a clean, minimal final stage — the build toolchain layers are entirely discarded from the final image.
-
FROM eclipse-temurin:21-jre-alpine — pick a minimal, version-pinned base image (never latest in production).
-
WORKDIR /app — all following relative paths resolve here; also creates /app if it doesn't exist.
-
ARG JAR_FILE=target/*.jar — declare a build-time variable with a default, usable in this stage only.
-
COPY ${JAR_FILE} app.jar — copy the build artifact from the build context into the image.
-
ENV JAVA_OPTS="-Xms256m -Xmx512m" — bake a runtime default into the image, overridable at docker run -e.
-
EXPOSE 8080 — document the port the app listens on (does not publish it).
-
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:8080/actuator/health || exit 1 — let Docker/Kubernetes know when the app is actually ready to serve traffic, not just 'process started'.
-
ENTRYPOINT ["java","-jar","app.jar"] — fixed executable; CMD (if any) supplies extra default arguments.
-
Multi-stage builds for Java: a maven or gradle stage compiles the jar; a jre-alpine or distroless/java final stage runs it, cutting image size from 600MB+ to under 200MB and removing the entire build toolchain (and its CVEs) from the attack surface.
-
Multi-stage builds for frontend: a node stage runs npm run build; an nginx:alpine final stage serves only the compiled static assets — Node.js itself never ships to production.
-
ARG for environment-specific base image selection in CI: --build-arg BASE_TAG=21-jre-alpine lets one Dockerfile serve dev/staging/prod with different JDK minor versions.
-
HEALTHCHECK integrated with Spring Boot Actuator's /actuator/health endpoint so orchestrators stop routing traffic to a container that's running but not yet ready (e.g., still warming caches).
-
Always prefer COPY over ADD unless you specifically need automatic tar extraction or a remote URL fetch — ADD's 'magic' behavior is a common source of confusing, hard-to-review Dockerfiles.
-
Always use exec form (JSON array) for CMD/ENTRYPOINT so the process becomes PID 1 directly and receives signals (SIGTERM) correctly for graceful shutdown.
-
Pin exact base image tags (eclipse-temurin:21.0.3_9-jre-alpine), not just major versions, for reproducible production builds; combine with Dependabot/Renovate to manage updates deliberately.
-
Use multi-stage builds for every compiled language (Java, Go, Node with a build step) — there is almost never a good reason to ship a build toolchain to production.
-
Create and switch to a non-root USER before the final ENTRYPOINT in every production image (Chapter 7).
-
Using ADD for a plain local file copy out of habit — this is harmless until someone later adds a filename that happens to look like a tar archive, triggering unexpected extraction.
-
Writing ENTRYPOINT java -jar app.jar (shell form) and then being confused why docker stop always takes the full 10-second grace period before SIGKILL — the shell absorbs SIGTERM instead of forwarding it.
-
Expecting an ARG value to be available inside the running container without also assigning it to ENV.
-
Believing EXPOSE actually opens a port on the host — it's purely documentation; only -p/--publish (or Compose ports:) does real port mapping.
-
Defining CMD as a full command (CMD java -jar app.jar) alongside an unrelated ENTRYPOINT, not realizing CMD is appended as arguments to ENTRYPOINT, producing an invalid combined command.
-
Use RUN mvn dependency:go-offline (or npm ci, or pip install with a separate requirements.txt COPY) as an isolated, cacheable layer before copying full source code.
-
Use BuildKit's --mount=type=cache,target=/root/.m2 (or npm/pip equivalents) to persist package manager caches across builds without baking them into any image layer.
-
Use --target to build and test only an intermediate stage during local development, skipping the final minimal-image stage until you're ready to ship.
-
Final production stage should contain only the runtime (JRE, not JDK) and the application artifact — no compilers, no package manager caches, no shell history.
-
Always define a HEALTHCHECK so the orchestration layer (Docker Swarm, Kubernetes readiness/liveness, or even a simple docker-compose restart policy) has a real signal of application health, not just process-alive status.
-
Bake sensible JVM defaults (heap sizing aware of container memory limits via -XX:+UseContainerSupport, now default in modern JDKs) into ENV/ENTRYPOINT rather than relying on every deployment environment to set them correctly.
-
Write a single-stage Dockerfile for a Spring Boot app, then convert it to a multi-stage build, and compare docker images sizes.
-
Deliberately write ENTRYPOINT in shell form, run the container, and time how long docker stop takes versus after switching to exec form.
-
Add a HEALTHCHECK pointed at /actuator/health and observe the health status transition in docker ps (starting -> healthy).
-
Use docker build --target builder -t debug-image . on a multi-stage Dockerfile and shell into it to inspect intermediate build artifacts.
-
FROM starts a stage; multiple FROM lines enable multi-stage builds.
-
RUN/COPY/ADD create layers; WORKDIR/ENV/ARG/EXPOSE/CMD/ENTRYPOINT/HEALTHCHECK are mostly metadata (no new filesystem layer, except where noted).
-
COPY is predictable; ADD adds remote-fetch and auto-extract — prefer COPY unless you need those.
-
ENTRYPOINT = fixed executable; CMD = default/overridable arguments; always use exec (JSON array) form for correct signal handling.
-
ARG is build-time only; promote to ENV explicitly if the running container needs the value.
-
EXPOSE documents; -p publishes. Multi-stage builds are the default way to keep production images minimal and secure.
Want a visual for this concept?
Generate a diagram tailored to “Dockerfile Instructions & Multi-Stage Builds” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →