Skip to content

VOL 00 / CH 07 / LESSON 02

7.2 Write a Runnable Dockerfile ​

Prerequisites: Images, Containers, and Run Boundaries

Estimated time: 20 minutes

Exercise environment: Docker Engine; access to the registry is required when using BusyBox for the first time

You are ready to continue when: you can explain build context, caching, start command, and non-root users, and build a minimal image

You can now run an existing image. Write a Dockerfile to make your own application files, dependencies, user, and startup command reviewable.

A Dockerfile describes how to build an image. It doesn't copy your entire computer, and it doesn't automatically include files outside of the build context.

A Runnable Minimum Project ​

Start by creating an isolated directory, keeping all build context inside it:

bash
image_workdir=$(mktemp -d)
image_tag="workshop-ch07-message:${RANDOM}-${RANDOM}"
mkdir -p "$image_workdir/workshop-image"
cd "$image_workdir/workshop-image"

Place three files in the directory:

text
workshop-image/
├── Dockerfile
├── .dockerignore
└── message.txt

message.txt:

text
portable workshop

Dockerfile:

dockerfile
FROM busybox:latest
WORKDIR /app
COPY --chown=65532:65532 message.txt .
USER 65532:65532
ENTRYPOINT ["cat"]
CMD ["/app/message.txt"]

.dockerignore:

text
.git
*.log
.env
bash
cd "$image_workdir/workshop-image"
docker build -t "$image_tag" .
docker run --rm "$image_tag"
docker run --rm --entrypoint id "$image_tag" -u

You should first see portable workshop, followed by the numeric user 65532. The COPY --chown and numeric UID are relevant for Linux containers; Windows containers use a different user and permission model. The numeric UID does not need to have a corresponding user account in the image. If the application relies on the home directory or user database, explicitly create the user.

What Does Each Instruction Do ​

InstructionStageFunction
FROMBuildSelect a base image and start the build stage
WORKDIRBuild and Run ConfigurationSet up subsequent instructions and the default starting directory
COPYBuildCopy files from the build context; cannot read arbitrary host paths beyond the context
RUNBuildExecute build steps and form the result layer
USERBuild and Run ConfigurationSet the user for subsequent RUN, as well as the user and group for the default container process
EXPOSEMetadataDescribe the ports that the application listens on, without publishing the ports
ENTRYPOINTRun ConfigurationDefine the main executable
CMDRun ConfigurationDefine the default command, or provide default parameters for ENTRYPOINT

Prefer the JSON exec form, such as CMD ["java", "-jar", "app.jar"]. It does not add an extra layer of shell, the parameter boundaries are clear, and the main process is easier to receive signals sent by docker stop. If a shell form is indeed needed, understand the impact of PID 1 and exec.

Build Context with .dockerignore ​

The final . in docker build -t name . specifies the build context, not merely the directory containing the Dockerfile. The source path for COPY is parsed from the context, and the Docker client may also need to send the context to the remote builder.

Use .dockerignore to exclude .git, logs, test artifacts, dependency directories, and .env. It can minimize context, reduce cache invalidations, and lower the chance of accidentally sending secrets into the builder. Don't treat it as a secret management system; credentials should use BuildKit secret mount, not ARG, ENV, or regular COPY.

Accurate Rules for Caching ​

latest is a movable tag, so repeated builds may use a different base image. Record and pin a reviewed digest when reproducing an exact environment. The builder checks whether each step can reuse its cache. COPY / ADD cache checks account for file contents and relevant metadata, but ignore modification time (mtime) itself; once a layer is invalidated, subsequent layers need to be rebuilt. Placing dependencies that change infrequently before sources that change frequently is usually beneficial, but "appearing unchanged" does not equate to a cache error.

Debugging steps:

bash
docker build --progress=plain -t "$image_tag" .
docker image inspect "$image_tag"
docker history "$image_tag"

First confirm the build context, labels, and actual running image. --no-cache can be used for diagnosis, but is not the preferred choice for daily cache troubleshooting; it also does not automatically pull updated base images.

Multi-Stage Skeleton for a Java Project ​

Build tools should be present during the build stage, and the run stage should only retain what is necessary for execution:

dockerfile
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /src
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn -B dependency:go-offline
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn -B package

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /src/target/app.jar /app/app.jar
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

This is a partial skeleton: it needs BuildKit support for RUN --mount, a complete POM and source tree, and the actual artifact name. app.jar must also have a valid Main-Class and bundled or configured runtime dependencies. An ordinary mvn package does not guarantee that java -jar will work. Multi-stage builds can exclude Maven and source code, but the final size of the image depends on the base image, dependencies, and artifacts, and cannot be guaranteed to be a specific number in advance. When releasing, it is also important to choose a supported version and use an immutable digest or supply chain strategy to control changes to the base image.

Clean Up This Lesson Resources ​

bash
docker image rm "$image_tag"
cd
ls -ld -- "$image_workdir"
rm -r -- "$image_workdir"

Only delete the tags and temporary directories created in this lesson. Do not use docker system prune as the command to clean up the tutorial, as it may affect the cache of other projects and stopped containers. Details are specified in Dockerfile reference.

Once the output and UID match the expected values, the single-image build and run exercise is complete. Next, use Compose to define a common project boundary for a group of services.

→ Next: Docker Compose

Built with VitePress | Software Systems Atlas