Why this matters
Every image your team ships is built from a Dockerfile: a text file with one instruction per line, read top to bottom by the builder. When a build is slow or fails, the build output tells you exactly which line and why - if you can read it. This lesson is the vocabulary for the rest of the chapter.
What you need to know already: images, layers and config (the two previous lessons), shell commands and exit codes (Ch 6, "The header every script gets"), apt (Ch 1).
The example app
Most examples in this chapter build orders, a small Java web service. Four Java words you need:
- JAR - a Java program packed into one zip file (
app.jar). You run it withjava -jar app.jar. - JRE - the Java Runtime: what you need to run a JAR.
- JDK - the Java Development Kit: the JRE plus the compiler, needed to build one.
- Maven (
mvn) - a Java build tool. It reads the project filepom.xml, downloads the libraries the app depends on (its dependencies) into~/.m2, compiles the code and writes the JAR intotarget/.
eclipse-temurin is a popular image with Java in it (21-jre, 21-jdk); maven is an image with Java plus Maven.
Every instruction, in one place
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jre # base image. Every stage starts with one
ARG APP_VERSION=dev # build-time variable (--build-arg)
ENV SPRING_PROFILES_ACTIVE=prod # runtime variable, stored in the image
WORKDIR /app # cd, creating the directory if needed
COPY target/app.jar app.jar # files from the build context
ADD https://example.com/x.tar.gz /opt/ # COPY plus URLs and tar extraction - avoid
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* # run a command, snapshot the result
USER 1000:1000 # who RUN, CMD and ENTRYPOINT run as from here
EXPOSE 8080 # documentation only
HEALTHCHECK CMD curl -f http://localhost:8080/actuator/health || exit 1
LABEL org.opencontainers.image.source=https://github.com/ing/orders
STOPSIGNAL SIGTERM # what docker stop sends
ENTRYPOINT ["java","-jar","app.jar"] # the executable
CMD ["--server.port=8080"] # default arguments to it
One line at a time:
# syntax=...- which version of the Dockerfile language to use (first line only).FROM- the base image to start from.ARG- a variable that exists only while building; set with--build-arg NAME=value.ENV- an environment variable (Ch 2, "Exec directives, environment and secrets") baked into the image and set in every container. This one is a setting the Java app reads.WORKDIR- the current directory for the following lines (created if missing).COPY- copy files from your project (the build context, below) into the image.ADD- like COPY, but also downloads URLs and unpacks tar files.RUN- run a shell command inside the image being built and keep what it changed.USER- the user the following lines and the container run as.EXPOSE- notes which port the app listens on.HEALTHCHECK- a command Docker runs to ask "is the app OK?" (/actuator/healthis orders' health URL). Its own lesson comes later.LABEL- a key=value note stored in the image, here the source repo.STOPSIGNAL- the signal (Ch 3, "Signals") thatdocker stopsends.ENTRYPOINT/CMD- the program the container runs, and its default arguments. Its own lesson comes later.
Two groups, and the difference drives the rest of the chapter:
- Filesystem instructions -
FROM,RUN,COPY,ADD,WORKDIR- change files and produce a layer. - Metadata instructions -
ENV,ARG,USER,EXPOSE,LABEL,CMD,ENTRYPOINT,HEALTHCHECK,STOPSIGNAL- only change the image config. Zero bytes, but they still count for the cache (the cache lesson).
ADD surprises people: a tarball you meant to copy gets unpacked, and URL downloads cannot be verified. Use COPY, and RUN curl when you really need a download.
EXPOSE publishes nothing. It records "this image listens on 8080" for humans and for docker run -P. Only docker run -p 8080:8080 (publish: forward host port 8080 to the container's port 8080) opens a port on the host.
Reading BuildKit output
docker build -t orders:1 . builds an image from the Dockerfile in the current directory. -t orders:1 tags the result (names it); the . is the build context.
$ docker build -t orders:1 .
[+] Building 74.6s (11/11) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 243B 0.0s
=> [internal] load metadata for docker.io/library/maven:3.9-eclipse-temurin-21 1.1s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/4] FROM docker.io/library/maven:3.9-eclipse-temurin-21@sha256:0c7f714b... 29.0s
=> [internal] load build context 0.0s
=> => transferring context: 1.52kB 0.0s
=> [2/4] WORKDIR /app 0.0s
=> [3/4] COPY . . 0.1s
=> [4/4] RUN mvn package -DskipTests 71.7s
=> exporting to image 0.2s
=> => exporting layers 0.1s
=> => writing image sha256:cf3ab9a158c8214341caa63372af70c79291dc8c3819fa4f705... 0.0s
=> => naming to docker.io/library/orders:1 0.0s
The first line: total time, steps done / steps total. Then line by line:
- load build definition - the Dockerfile itself was sent to the builder.
- load metadata for ... - the builder asked the registry what the tag currently points at. This is also where "pull access denied" and typos in
FROMfail. - load .dockerignore - 2B here means there is no .dockerignore (the context lesson explains it).
- FROM ...@sha256: - the base was resolved to a digest. The first time, you also see one
sha256:... 101MB / 101MBline per layer being downloaded. - load build context / transferring context - how much of your directory was sent. The number to watch.
- [2/4], [3/4] - only filesystem steps are numbered.
ENVand friends do not get a line. mvn package -DskipTests- Maven builds the JAR;-DskipTestsskips running the tests.- The time on the right of each step is where your build time went. Here it is all in
mvn package, which is what the cache lesson fixes. - exporting / writing image / naming - the result is saved and tagged.
In a Dockerfile with several FROM lines (a multi-stage build, later) the step labels carry the stage name: [build 3/6] COPY pom.xml ., [stage-1 2/3] WORKDIR /app.
When you need the actual output: --progress=plain
The normal view hides what each RUN prints. --progress=plain shows it, with the seconds since the step started. --no-cache forces every step to run again (the cache lesson explains why it normally would not):
$ docker build --progress=plain --no-cache -t orders:1 .
#0 building with "default" instance using docker driver
#7 [4/4] RUN mvn package -DskipTests
#7 0.100 [INFO] Scanning for projects...
#7 0.431 [INFO] Building orders 2.14.1
#7 9.874 [INFO] BUILD SUCCESS
#7 DONE 71.7s
#7 is the step's number in this build; every line of that step starts with it. When a build fails on a build server, plain output is what you want in the logs.
When a step fails
=> ERROR [4/4] RUN mvn package -o 2.1s
------
> [4/4] RUN mvn package -o:
2.100 [ERROR] Failed to execute goal on project orders: Could not resolve dependencies ...
------
Dockerfile:4
--------------------
2 | WORKDIR /app
3 | COPY . .
4 | >>> RUN mvn package -o
5 | ENTRYPOINT ["java","-jar","target/orders-2.14.1.jar"]
--------------------
ERROR: failed to solve: process "/bin/sh -c mvn package -o" did not complete successfully: exit code: 1
Three parts: the last lines of the failing command's output (here mvn -o, offline mode, could not download what it needed), the Dockerfile line with >>>, and the exit code. exit code: 127 is the shell's "command not found" (Ch 6): /bin/sh: 1: mvn: not found means the tool is not in that base image.
Build checks
BuildKit checks the Dockerfile for common mistakes as it builds, and prints warnings at the end:
2 warnings found (use docker --debug to expand):
- JSONArgsRecommended: JSON arguments recommended for ENTRYPOINT to prevent unintended behavior related to OS signals (line 5)
- SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "DB_PASSWORD") (line 3)
Both of those are real bugs that this chapter spends a mission on each. Others you will see: FromAsCasing (FROM x as build - make the AS match), StageNameCasing, LegacyKeyValueFormat (ENV KEY value instead of ENV KEY=value), WorkdirRelativePath, MaintainerDeprecated.
docker build --check . runs only the checks, without building:
# in a project whose ENTRYPOINT is still shell form
docker build --check .
JSONArgsRecommended - https://docs.docker.com/go/dockerfile/rule/json-args-recommended/
JSON arguments recommended for ENTRYPOINT to prevent unintended behavior related to OS signals
Dockerfile:5
--------------------
3 | COPY . .
4 | RUN mvn package -DskipTests
5 | >>> ENTRYPOINT java -jar target/orders-2.14.1.jar
--------------------
It costs a second; put it next to your other checks on every commit.
The build context, in one sentence
The last argument to docker build (the .) is the build context: a directory whose entire contents are sent to the builder. COPY can only see files in it, and everything in it that is not excluded by .dockerignore is transferred on every build. The context lesson is about why that matters.
What you can now do
- Say what each Dockerfile instruction does, and which ones make layers.
- Read BuildKit output: the steps, the context size, the time per step.
- Find the failing line and its exit code when a build breaks.