Building a Dockerfile One Line at a Time
This lab runs on a real VM
This box is not a Pod but a virtual machine launched by KubeVirt. A separate Linux kernel runs in it, systemd actually manages services, and docker is a real Docker engine, not an imitation. A container started with docker run becomes a real process, and both docker exec and docker logs work as usual.
This lab used to run inside a Pod. That box had dropped every kernel capability, so the step that starts a container was blocked, and you learned by working around it and unpacking image archives by hand. The workaround is no longer needed.
There are two things to know.
- The first start takes a little over a minute. The VM boots and installs Docker, so it is slower than a Pod lab (usually 40 seconds).
- There is no browser preview. Only one grading port is open for connections into the VM. If you start a web server, check it with
curlfrom inside the VM.
Goal
You add Dockerfile instructions one at a time, check with inspect which part of the image each one changes, and finally create a file that passes the review checklist.
Why it matters
A Dockerfile looks like a configuration file, but it is actually a build script in which order carries meaning. Even with the same instruction, depending on its position the layers grow, the cache breaks, and secrets get baked in. The purpose of this lab is not to memorize the instructions but to build the habit of asking each time, "does this line create a layer or stamp a configuration?" That one distinction prevents most size problems and security problems.
Steps
At each step, build with a new tag so that the results of the earlier steps are kept.
- Create
/root/build1and write the first line of/root/build1/DockerfileasFROM alpine:3.20. - Build it as
labhub/app:v1. - Create
/root/build1/app.shso that it printsapp-running, addWORKDIR /appandCOPY app.sh /app/to the Dockerfile, and build it aslabhub/app:v2. - Add
CMDin JSON array form so thatapp.shruns, and build it aslabhub/app:v3. When you run it,app-runningshould appear. - Add
ENV APP_ENV=prodandARG VERSION, and stamp the value received through ARG intoLABEL app.version. Using--build-arg VERSION=2.1.0, buildlabhub/app:v4. - Set
ENTRYPOINTin array form andCMD ["default-arg"]as the default, so that running without arguments printsdefault-argand passingoverride-argprintsoverride-arg, then build it aslabhub/app:v5. - Add
LABEL org.opencontainers.image.title=labhub-appandEXPOSE 8080, and build it aslabhub/app:v6. - Polish the final
/root/build1/Dockerfileso that it satisfies all five items below, and build it aslabhub/app:v7.FROMhas a pinned tag and it is notlatestWORKDIRcomes before the firstCOPYENTRYPOINTorCMDis a JSON arrayENV/ARGnames do not contain TOKEN, SECRET, PASSWORD or API_KEY
Notes
- You can see the configuration fields at a glance with
docker image inspect labhub/app:v4 | jq '.[0].Config'. - Pass the argument in the form
docker build --build-arg VERSION=2.1.0 -t labhub/app:v4 /root/build1. - Common mistake 1: if you write it in shell form like
CMD app.sh, it becomes a single string instead of an array. - Common mistake 2: in step 5, if you pass an ARG value on to ENV, it remains in the image as it is. Stamp it with LABEL.
Pin the base image
Create /root/build1 and write the first line of /root/build1/Dockerfile as FROM alpine:3.20.
Specify a tag in FROM. latest makes yesterday's build and today's build differ.
First build
Build it as labhub/app:v1.
For the build context, specify the directory that contains the Dockerfile. If you do not name it with -t, it is hard to find later.
Working directory and copying files
Create /root/build1/app.sh so that it prints app-running, add WORKDIR /app and COPY app.sh /app/ to the Dockerfile, and build it as labhub/app:v2.
WORKDIR is stamped into the image configuration (Config.WorkingDir), and COPY creates a layer. The order of the two instructions determines the meaning of relative paths.
Specify the default command
Add CMD in JSON array form so that app.sh runs, and build it as labhub/app:v3. When you run it, app-running should appear.
In the shell form it is wrapped in /bin/sh -c and the shell becomes PID 1. You saw why the array form is needed in the previous course.
The difference between build arguments and environment variables
Add ENV APP_ENV=prod and ARG VERSION, and stamp the value received through ARG into LABEL app.version. Using --build-arg VERSION=2.1.0, build labhub/app:v4.
ARG lives only while building, and ENV remains in the image. To keep a value received as a build argument in the image, stamp it with LABEL.
Combining ENTRYPOINT and CMD
Set ENTRYPOINT in array form and CMD ["default-arg"] as the default, so that running without arguments prints default-arg and passing override-arg prints override-arg, then build it as labhub/app:v5.
ENTRYPOINT is fixed, and CMD is replaced by the arguments given at run time. Write both in array form.
Standard label and port declaration
Add LABEL org.opencontainers.image.title=labhub-app and EXPOSE 8080, and build it as labhub/app:v6.
OCI standard label keys start with org.opencontainers.image. EXPOSE does not open a port; it is an instruction that documents it.
Pass the review checklist
Polish the final /root/build1/Dockerfile so that it satisfies all five items below, and build it as labhub/app:v7.
FROMhas a pinned tag and it is notlatestWORKDIRcomes before the firstCOPYENTRYPOINTorCMDis a JSON arrayENV/ARGnames do not contain TOKEN, SECRET, PASSWORD or API_KEY
Polish the Dockerfile you have so far to fit the five items. Grading looks at both the file contents and the build result.