Lesson  in  Kubernetes 101

Images and layers

Build the first image of the api, open it up to see the layers it is made of and check with a stopwatch why the order of the instructions in a Dockerfile is not a matter of style.

The whole course deploys tienda/api:2.1.0 as if it came ready-made. It doesn't: someone built it, and how they built it decides things that will show up much later, when you no longer see them coming.

An image is a package with everything needed to run an application: the base distribution, the libraries, your binary. It is built from a text file, the Dockerfile, and each instruction produces a read-only layer. The final image is that stack.

In this lesson you build the first version of the api, open it up to see its layers, and check with a stopwatch why the order of the instructions is not a matter of style. There is no cluster yet: work from the dev-machine tab.

Step 1: The first image

In ~/projects/api you have the code: an HTTP server in Go, standard library only, listening on 8080. It's missing the Dockerfile. Create it:

cat << 'EOF' > ~/projects/api/Dockerfile
FROM golang:1.24-alpine
WORKDIR /src
COPY . .
RUN apk add --no-cache git
RUN go build -o /api .
CMD ["/api"]
EOF

The Dockerfile, explained in questions and answers

What does each instruction contribute?

FROM picks the base image to stack on; WORKDIR sets the working directory; COPY puts files inside; RUN executes a command during the build and saves the result as a new layer; CMD declares which process starts when the container runs. Only CMD produces no layer: it is metadata, not content.

What is the apk add git for?

To stand in for what, in a real project, is installing system dependencies. It isn't needed to compile here, and that is exactly why it makes a clean example: an expensive layer that doesn't depend on your code.

Does the image carry a kernel inside?

No. The image provides the filesystem and the libraries; the kernel is always the host's. That is why an Alpine image runs on an Ubuntu host without trouble: all they share is the architecture and the operating system family.

Build it:

cd ~/projects/api
docker build -t tienda/api:1 .
docker images tienda/api

Step 2: Opening the stack

Now look at what it's made of:

docker history tienda/api:1

Each line is a layer, with the instruction that created it and how much it takes up. You'll see that a few of them account for almost all the size: the Go base, the apk add and the go build.

Notice the total. That's more than 350 MB for a server that writes one line of text, and inside goes the entire Go compiler, its standard library in source form and Alpine's package manager. None of that is needed to run anything. It is the problem the next lesson's challenge solves.

A surprising detail: deleting a file in a later layer doesn't remove it from the image. Layers stack, they don't get rewritten: the new layer covers it, and the content is still down there, taking up space and recoverable. That is why a secret copied into an image by mistake isn't fixed with a RUN rm.

Step 3: The cache, and why order matters

Layers get cached. When rebuilding, Docker reuses the ones above the first change and redoes only the rest. Check it: touch the code and rebuild, timing it.

sed -i 's/2.1.0/2.1.1/' main.go
time docker build -t tienda/api:2 .

Look at the output. As soon as it reaches the COPY . . the cache is over, and from there on it redoes everything: the apk add included, even though you haven't touched a single dependency. You've paid for the package install to change a literal.

The rule that follows is short: from least volatile to most volatile. What almost never changes goes at the top; your code, at the end. Reorder the Dockerfile:

cat << 'EOF' > ~/projects/api/Dockerfile
FROM golang:1.24-alpine
WORKDIR /src
RUN apk add --no-cache git
COPY . .
RUN go build -o /api .
CMD ["/api"]
EOF

Step 4: Checking that the cache now does its job

Build twice with the new Dockerfile, changing the code in between:

time docker build -t tienda/api:2 .
sed -i 's/2.1.1/2.1.2/' main.go
time docker build -t tienda/api:3 .

The second build takes a fraction of the first, and in the output you'll see CACHED on the instructions at the top. It isn't that Docker repeated the work faster: it didn't repeat it. The apk add layer of tienda/api:3 doesn't resemble the one in tienda/api:2: it is literally the same one, the same blob stored exactly once.

Check it by comparing the layer chain of the two images:

docker image inspect tienda/api:2 --format '{{range .RootFS.Layers}}{{println .}}{{end}}'
docker image inspect tienda/api:3 --format '{{range .RootFS.Layers}}{{println .}}{{end}}'

The first ones match one by one (the base and the apk add) and only the last two diverge, the ones that depend on your code. That is exactly what the task is going to check.

docker history is no use here: with BuildKit, the builder Docker uses by default, the intermediate layers show up as <missing> and there are no identifiers to compare. It is still good for seeing instructions and sizes, which is what you used it for in step 2.

Summary

  • An image is a stack of read-only layers; every Dockerfile instruction that adds content creates one.
  • Layers stack and don't get rewritten: what you delete in a later layer is still inside the image.
  • The cache reuses everything above the first change, so the order of the instructions decides whether rebuilding costs seconds or minutes.
  • From least volatile to most volatile: base and dependencies at the top, your code at the end.
  • And one problem is left unsolved: what you've built weighs more than 350 MB and carries the compiler inside. That is the next lesson.