Cutting It Down With Multi-Stage Builds
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 move only the output made in the builder stage into the final image, and confirm by size numbers the process of making the same result as a much smaller image.
Why it matters
Image size is not a matter of appearance but of deployment speed and rollback speed. Every time a node scales out, every time you roll back, you download that size again. And the biggest lever for reducing size is not compression or cleanup but "never creating, in that stage, what must not be in the final image". Because layers are not rewound, if you violate this principle, the size will not come back no matter how much you delete later.
Steps
- Create
/root/build3and make two stages in/root/build3/Dockerfile. The first stage usespython:3.12-alpine, is namedbuilder, and, in/out/app.txt, writesbuilt-in-builder. The second stage isalpine:3.20. Build it aslabhub/ms:v1. - Change the final stage so that it copies only the builder's
/out/app.txtto/app/app.txt, and build it aslabhub/ms:v2. When you run it and read the file,built-in-buildermust appear. - In
/root/build3/fat.Dockerfile, write a version that produces the same result in a single stage (withpython:3.12-alpineas the base) and build it aslabhub/ms:fat. - Build only the first stage to create the
labhub/ms:builderimage. It must contain/out/app.txt. - In the final stage of the
Dockerfile, add a line that, from thebusybox:1.36image, copies/bin/busyboxdirectly to/app/busybox, and build it aslabhub/ms:v3. - Run
labhub/ms:v3and confirm that there is nopython3. The image size must be under 60 MB. - Create
/root/build3/scratch.Dockerfilethat puts, on top ofFROM scratch, only the file frombusybox:1.36at/bin/busybox, and specifyENTRYPOINTin array form so that it prints the string it receives as an argument. Build it aslabhub/ms:scratch, and when you givescratch-worksas an argument, that string must be printed. - In
/root/build3/size.md, write the four lines below with the actual values.
fat_mb=<labhub/ms:fat 크기(MB)>
slim_mb=<labhub/ms:v3 크기(MB)>
scratch_mb=<labhub/ms:scratch 크기(MB)>
reduction_pct=<(fat_mb - scratch_mb) / fat_mb * 100 의 정수부>
Notes
- Build only the intermediate stage with
docker build --target builder -t labhub/ms:builder /root/build3. - You can also refer to an image that is not a stage, as in
COPY --from=busybox:1.36 /bin/busybox /app/busybox. - busybox is statically linked, so it works on scratch too. It is run in the form
busybox echo <문자열>(the placeholder stands for the string to print). - Common mistake 1: if you copy the whole stage directory in step 2, the size barely shrinks.
- Common mistake 2: if you write ENTRYPOINT in shell form in step 7, it fails immediately because scratch has no shell.
Create two stages
Create /root/build3 and make two stages in /root/build3/Dockerfile. The first stage uses python:3.12-alpine, is named builder, and, in /out/app.txt, writes built-in-builder. The second stage is alpine:3.20. Build it as labhub/ms:v1.
Write FROM twice and give the first stage a name. You will refer to the name later in COPY.
Bring only the output
Change the final stage so that it copies only the builder's /out/app.txt to /app/app.txt, and build it as labhub/ms:v2. When you run it and read the file, built-in-builder must appear.
Pick and copy only the file made in the builder stage. If you copy the whole stage, there is no point in using multi-stage.
Compare the size with a single stage
In /root/build3/fat.Dockerfile, write a version that produces the same result in a single stage (with python:3.12-alpine as the base) and build it as labhub/ms:fat.
Compare it with an image that leaves the same result as it is on a heavy base. It should shrink by 30% or more.
Build only the intermediate stage
Build only the first stage to create the labhub/ms:builder image. It must contain /out/app.txt.
When debugging, you can extract only the builder stage as a separate image. There is an option that stops before reaching the final stage.
Copy directly from another image
In the final stage of the Dockerfile, add a line that, from the busybox:1.36 image, copies /bin/busybox directly to /app/busybox, and build it as labhub/ms:v3.
--from does not accept only stage names. You can also give an image reference as it is.
Do not leave build tools behind
Run labhub/ms:v3 and confirm that there is no python3. The image size must be under 60 MB.
Actually run it and confirm that the runtime that was used only for the build has disappeared from the final stage.
Put it on an empty base
Create /root/build3/scratch.Dockerfile that puts, on top of FROM scratch, only the file from busybox:1.36 at /bin/busybox, and specify ENTRYPOINT in array form so that it prints the string it receives as an argument. Build it as labhub/ms:scratch, and when you give scratch-works as an argument, that string must be printed.
scratch has no shell, so ENTRYPOINT must be in array form. You have to choose a statically linked binary.
Size report
In /root/build3/size.md, write the four lines below with the actual values.
Measure the actual sizes of the three images, write them down and calculate the reduction rate. 1 MB = 1048576 bytes.