Containerization Lesson 2 of 113

Containers & Docker Basics: Images, Layers, and Registries

Before you can run anything on Kubernetes, you need the thing Kubernetes actually runs: a container. This is lesson 1 of the Kubernetes Zero-to-Hero course, and it deliberately starts one level below Kubernetes — with Docker, images, and registries — because every confusing thing you’ll hit later (“ImagePullBackOff”, “it works on my laptop but not in the cluster”, “why is my image 1.2 GB?”) traces straight back to the concepts here.

By the end you’ll know what a container really is (and how it differs from a virtual machine), the difference between an image and a container, the full Dockerfile → build → image → registry → run lifecycle, and why layer caching and base-image choice make builds fast or slow. Then you’ll build, run, and inspect a real image on your own laptop — free, no cloud account required.

Level: Beginner · Time: ~30 min · Cost: Free — everything runs on your laptop, no cloud account.

In a nutshell

Strip away the buzzwords and a container is just a normal program running on your computer — with one twist: it’s been handed its own private view of the system. It sees its own files, its own network, its own list of running processes, as if it were alone on a brand-new machine. In reality it’s sharing your computer with everything else, fenced off so it can’t see or touch its neighbours.

Think of an apartment building. The building is your machine — and its single Linux kernel is the shared foundation, plumbing, and wiring everyone relies on. Each apartment is a container: it has its own front door, its own furniture, its own address, and the tenant inside feels like they have a whole home. They don’t each pour their own foundation or drill their own well — that would be a house (a virtual machine). Apartments are cheaper and faster to build precisely because they share the structure underneath.

Two Linux features build those apartment walls, and they’re worth naming once in plain terms:

Everything else in this lesson — images, layers, registries, Dockerfiles — is just the machinery for packaging one of these programs so it runs identically on your laptop, a colleague’s laptop, and a server in the cloud. That “runs the same everywhere” property is why containers took over, and why Kubernetes (which runs containers by the thousand) exists at all.

Learning objectives

By the end of this lesson you will be able to:

Prerequisites & where this fits

You need only basic comfort with a terminal — cd, ls, editing a small text file — and Docker Desktop (macOS/Windows) or Docker Engine / Podman (Linux) installed. No Kubernetes, no cloud account, no prior container experience is assumed.

This is the first lesson in the Kubernetes Fundamentals module of the Kubernetes Zero-to-Hero course. Everything Kubernetes orchestrates is a container, so we build that foundation here. The next lesson, What Is Kubernetes? Control Plane, Nodes, etcd & the kubelet, introduces the cluster that schedules and runs these images at scale.

What a container actually is (vs a virtual machine)

A container packages an application with everything it needs to run — code, runtime, libraries, system tools — into one isolated, portable unit that behaves the same on your laptop, a CI runner, and a production server. That portability (“build once, run anywhere”) is the whole point.

The usual starting comparison is a virtual machine (VM). A VM virtualizes hardware: a hypervisor (VMware, Hyper-V, KVM) splits a physical machine into VMs, each running its own full operating system — its own kernel, its own boot process. That’s powerful but heavy: gigabytes in size, tens of seconds to boot.

A container virtualizes the operating system instead. All containers on a host share the host’s Linux kernel — there’s no second OS to boot. Each container just gets its own isolated view of the system (its own filesystem, process tree, and network), so it feels like it owns the machine while really being a set of normal processes behind strong fences. The result: megabytes, not gigabytes, and startup in milliseconds.

That isolation comes from two Linux kernel features. Docker sets them up for you, but knowing the names demystifies a lot:

In one line: a container is a normal process, boxed in by namespaces (its view of the world) and cgroups (its resource budget), sharing the host kernel.

Virtual machine Container
Isolates by virtualizing Hardware (own kernel + OS) The OS (shares host kernel)
Typical size Gigabytes Megabytes
Start time Tens of seconds Milliseconds
Isolation strength Stronger (separate kernel) Strong, but shared kernel
Density per host Tens Hundreds to thousands

The trade-off is real: sharing the host kernel makes the isolation boundary thinner than a VM’s. That’s fine for most workloads and is exactly what makes containers cheap and fast — but it’s why dropping privileges and (at the high end) sandboxed runtimes exist. We touch security at the end of this lesson.

Images vs containers: template vs running instance

This is the most common point of confusion for beginners, so let’s nail it.

An image is a read-only template — an immutable snapshot of a filesystem plus a little metadata (which command to run, which ports to expose). It does nothing on its own; it just sits there, like a .iso file or a class definition in code.

A container is a running instance of an image. docker run takes that read-only template, adds a thin writable layer on top, and starts the process inside. You can start many containers from one image — like creating many objects from one class — and each gets its own writable layer, so one container’s changes don’t affect the image or the others.

Image Container
Read-only template Running (or stopped) instance
Immutable; built once Has a writable top layer
Like a class / blueprint Like an object / a process
Stored in a registry Lives on a host while it runs
docker build produces it docker run creates it

Under the hood this is copy-on-write (CoW): the container reads files straight from the shared read-only image layers, and only when it modifies a file is a private copy made in its own writable layer. That’s why launching ten containers from one image is nearly instant and costs almost no extra disk — they share every unchanged byte until the moment they diverge.

A practical consequence: containers are ephemeral. Anything written inside a running container’s writable layer disappears when the container is removed. That’s by design — it’s why we keep state in volumes and databases, not inside containers — and it’s a foundational idea you’ll meet again the moment you learn about Pods in Kubernetes.

The lifecycle: Dockerfile → build → image → registry → run

Here is the path every container takes, from a text file on your machine to a running process. This is the backbone of the whole lesson.

The Docker image lifecycle: a Dockerfile is built into a layered, read-only image, pushed to a registry, then pulled and run as a container.

As the diagram shows, a Dockerfile (your recipe) is turned by docker build into a layered, read-only image; that image is pushed to a registry (a shared store); later, any host pulls the image and runs it as a container. Let’s walk each stage.

1. The Dockerfile (the recipe)

A Dockerfile is a plain-text file of instructions describing how to assemble an image. A minimal one for a tiny static site looks like this:

# syntax=docker/dockerfile:1
FROM nginx:1.27-alpine          # 1. start FROM a base image
COPY index.html /usr/share/nginx/html/index.html   # 2. add our content
EXPOSE 80                       # 3. document the port the app listens on
# nginx's own image already defines the start command (CMD), so we inherit it

The instructions you’ll meet first:

2. docker build → an image made of layers

docker build executes the Dockerfile top to bottom and produces an image. The crucial detail: each instruction creates a new read-only layer, stacked on the one before. An image is therefore an ordered stack of layers, each a diff (a set of filesystem changes) over the previous one.

┌─────────────────────────────┐  ← COPY index.html   (your content)
├─────────────────────────────┤  ← nginx config / binaries
├─────────────────────────────┤  ← Alpine base packages
└─────────────────────────────┘  ← FROM nginx:1.27-alpine (base)

Layers are content-addressed and shared between images. If ten of your images all start FROM node:20, that base is stored once on the host and reused — saving disk and download time.

How the layers become one filesystem — the union mount. A running container doesn’t see four separate layers; it sees a single, ordinary-looking filesystem. That illusion is created by a union (overlay) filesystem — on Linux, usually the overlay2 storage driver, backed by the kernel’s OverlayFS. It stacks all the read-only image layers plus the container’s writable layer and presents a merged view: when two layers hold the same path, the upper one wins. Reads come from whichever layer contains the file; writes land in the top writable layer via copy-on-write. This is the same mechanism that lets image layers be shared safely across many containers and images — the read-only layers are only ever mounted, never modified.

3. Push to a registry

A built image lives only on the machine that built it until you push it to a registry — a server that stores and distributes images — making it available to teammates, CI, and clusters.

4. Pull and run

On any other host, docker run (or, later, Kubernetes) will pull the image if it isn’t already present, then start a container from it. Same image, same behavior, anywhere.

Keep this loop in your head: author the Dockerfile, build the image, push to a registry, pull & run as a container. When something breaks in Kubernetes later, you’ll almost always be debugging one of these four stages.

Tags, base images, and layer caching

These three ideas separate someone who “can build an image” from someone who builds good images.

Tags: naming and versioning images

An image reference has the shape registry/repository:tag, e.g. ghcr.io/acme/web:1.4.2:

A tag is just a movable label, not a guaranteed version. latest is the classic trap: it doesn’t mean “newest and stable,” it’s simply the default tag and can be re-pointed to different content over time. In production, pin specific tags (1.4.2) or, for true immutability, pin by digest (web@sha256:...), which refers to exact bytes and can never move. Treat latest as “unspecified.”

Base images: what you build FROM

Your base image dictates your image’s size, attack surface, and which tools are available inside. Common choices:

Base Size (rough) When to use
ubuntu / debian ~70–120 MB Familiar, lots of packages, easy debugging
*-slim (e.g. python:3.12-slim) ~40–80 MB Trimmed distro; a sensible default
alpine ~5–10 MB Tiny; uses musl libc (occasional compatibility quirks)
distroless / scratch near-zero No shell or package manager; smallest + most secure for compiled apps

Smaller is generally better — less to download, fewer packages that can carry vulnerabilities — but it also means fewer debugging tools inside, so it’s a trade-off. Prefer official images and pin a real version tag (python:3.12-slim, not python:latest).

Layer caching: why instruction order matters

Because layers are diffs, Docker caches them. On a rebuild, Docker reuses a cached layer as long as that instruction and everything before it is unchanged. The moment one instruction changes, that layer and every layer after it must be rebuilt.

The practical rule: put rarely-changing things early, frequently-changing things late. The classic case is dependency install vs. copying source:

# syntax=docker/dockerfile:1
FROM node:20-slim
WORKDIR /app

# 1. Copy ONLY the dependency manifests first, then install.
#    These files change rarely, so this expensive layer stays cached.
COPY package.json package-lock.json ./
RUN npm ci

# 2. Copy the source LAST. It changes on every commit, but it's a
#    cheap layer, and the npm install above is reused from cache.
COPY . .
CMD ["node", "server.js"]

If you instead did COPY . . before RUN npm ci, then any one-character change to your source would invalidate the cache and force a full reinstall of dependencies on every build — slow and wasteful. Ordering your Dockerfile for the cache is one of the highest-leverage habits in containerization. (For the next level — multi-stage builds and BuildKit’s smarter cache — see Multi-stage Dockerfiles & BuildKit Cache Optimization.)

Registries: where images live

A container registry is a server that stores, versions, and distributes images. You push to it and pull from it. The ones you’ll encounter:

Registry Who runs it Typical use
Docker Hub (docker.io) Docker The default; home of most official base images
GHCR (ghcr.io) GitHub Images built from GitHub repos / Actions
Amazon ECR AWS Private images for workloads on AWS
Azure ACR Azure Private images for workloads on Azure
Google Artifact Registry Google Cloud Private images for workloads on GCP

Registries can be public (anyone can pull) or private (pull requires authentication). The cloud registries (ECR/ACR/Artifact Registry) hold a team’s own application images close to where they run; Docker Hub is where you pull base images. One note for later: Docker Hub rate-limits anonymous pulls, a real cause of ImagePullBackOff errors you’ll meet in a few lessons.

Hands-on lab

You’ll build a tiny image, run it, inspect its layers, and clean up. Everything here is free and runs entirely on your laptop — no cloud account, no cluster yet. We skip pushing to a remote registry (to stay zero-cost and zero-signup); the optional step shows how using a local registry container.

0. Verify Docker is working

docker version
docker run --rm hello-world

Expected (trimmed): the hello-world container prints a confirmation message, including:

Hello from Docker!
This message shows that your installation appears to be working correctly.

If you use Podman, every docker command below works as podman — the CLI is compatible.

1. Create a tiny project

mkdir kv-docker-lab && cd kv-docker-lab

cat > index.html <<'EOF'
<!doctype html>
<h1>Hello from KloudVin 🐳</h1>
<p>Served by nginx inside a container.</p>
EOF

cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM nginx:1.27-alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
EOF

2. Build the image

docker build -t kv-web:1.0 .

Expected output ends with lines like:

 => => naming to docker.io/library/kv-web:1.0
 => => writing image sha256:...

You just turned a Dockerfile into a tagged image, kv-web:1.0. Confirm it exists:

docker images kv-web
REPOSITORY   TAG   IMAGE ID       CREATED         SIZE
kv-web       1.0   a1b2c3d4e5f6   2 seconds ago   53.2MB

3. Run a container from the image

docker run -d --name kv-web -p 8080:80 kv-web:1.0

Now visit http://localhost:8080 in a browser, or:

curl -s http://localhost:8080

Expected:

<!doctype html>
<h1>Hello from KloudVin 🐳</h1>
<p>Served by nginx inside a container.</p>

You now have a running container (an instance) created from an image (the template). Confirm it’s running:

docker ps
CONTAINER ID   IMAGE        COMMAND                  STATUS         PORTS                  NAMES
f9e8d7c6b5a4   kv-web:1.0   "/docker-entrypoint.…"   Up 5 seconds   0.0.0.0:8080->80/tcp   kv-web

4. Inspect the layers

See exactly how the image was assembled, layer by layer:

docker history kv-web:1.0
IMAGE          CREATED          CREATED BY                                      SIZE
a1b2c3d4e5f6   2 minutes ago    COPY index.html /usr/share/nginx/html/index…   141B
<missing>      3 weeks ago      /bin/sh -c #(nop)  EXPOSE 80                     0B
<missing>      3 weeks ago      /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B
...
<missing>      3 weeks ago      /bin/sh -c #(nop) ADD file:... in /            8.4MB

The top line is your COPY layer (tiny — just your HTML); everything below it came from the nginx:1.27-alpine base image. The <missing> IDs are simply the base image’s own layers, which don’t have local build records. This is the layer stack from earlier, made real.

To see the writable layer caching pay off, rebuild without changing anything:

docker build -t kv-web:1.0 .

Every step now reports CACHED and the build finishes almost instantly — proof that unchanged instructions reuse cached layers.

5. Look inside the running container (optional)

docker exec -it kv-web sh
# now you're inside the container:
ls /usr/share/nginx/html
cat /etc/os-release   # note: this is Alpine, from the base image
exit

This demonstrates the isolated filesystem view from the namespaces discussion — inside the container, you see its world, not your laptop’s.

6. Validation

You’re done with the core lab if all of these are true:

7. (Optional) Push to a local registry — still free

If you want to feel the push/pull half of the lifecycle without any cloud signup, run a registry as a container on your own machine:

docker run -d -p 5000:5000 --name registry registry:2     # a local registry
docker tag kv-web:1.0 localhost:5000/kv-web:1.0           # retag for that registry
docker push localhost:5000/kv-web:1.0                     # push
docker rmi kv-web:1.0 localhost:5000/kv-web:1.0           # remove local copies
docker pull localhost:5000/kv-web:1.0                     # pull it back

That’s the exact tag → push → pull flow you’d use with Docker Hub or a cloud registry, just pointed at localhost.

8. Cleanup

docker rm -f kv-web                    # stop + remove the container
docker rmi kv-web:1.0                  # remove the image
docker rm -f registry 2>/dev/null      # remove the local registry (if you ran step 7)
docker rmi registry:2 2>/dev/null      # and its image
docker rmi localhost:5000/kv-web:1.0 2>/dev/null
cd .. && rm -rf kv-docker-lab          # remove the project folder
docker system prune -f                 # reclaim dangling layers/build cache

Cost note: Free / local. Docker Desktop (personal use), Podman, and a local registry container all run on your own machine at no charge. Nothing in this lab provisions a cloud resource, so there is nothing to bill and nothing to leave running. The docker system prune -f at the end reclaims any leftover disk space.

Going deeper

Everything above is enough to be productive. This section is for when you want to know what’s actually happening under the hood — the material that separates “I can run docker build” from “I understand containers.”

Namespaces and cgroups, concretely

When Docker “creates a container,” it’s really asking the Linux kernel to start a process inside a fresh set of namespaces and attach it to a cgroup. There’s no dedicated “container” object in the kernel at all — a container is that bundle of primitives. The namespace types you get:

Namespace Isolates So the container gets…
pid Process IDs Its own PID 1; it can’t see host processes
net Network stack Its own interfaces, IPs, ports, routing table
mnt Mount points Its own filesystem root and mounts
uts Hostname / domain Its own hostname
ipc Shared memory / semaphores Its own IPC objects
user UID/GID mapping Root inside mapped to non-root outside (the basis of rootless)
cgroup cgroup root Its own view of the cgroup hierarchy
time Certain system clocks Its own boot/monotonic clock offsets

cgroups (control groups, now almost always cgroups v2) are the other half. They form a hierarchy that meters and caps resources: --memory=256m sets a hard memory ceiling (exceed it and the kernel OOM-kills the process), --cpus=1.5 limits CPU time, and there are controllers for block I/O, PIDs, and more. When Kubernetes enforces resource requests and limits later, it is ultimately writing cgroup values — the same mechanism, one layer up.

You can see it yourself: inside a container, ps aux shows only the container’s processes (pid namespace), hostname shows the container ID (uts namespace), and cat /sys/fs/cgroup/memory.max shows the memory cap (cgroups v2).

The OCI standards: why any of this is portable

Early on, “a container image” meant “a Docker image,” which worried everyone who didn’t want one vendor to own the format. The Open Container Initiative (OCI) fixed that with three open specifications that today’s whole ecosystem implements:

The practical upshot: in 2026, “Docker image,” “OCI image,” and “container image” are essentially synonyms. You are never locked into one tool.

Docker vs containerd vs Podman (and where runc fits)

Beginners meet “Docker” and assume it’s one monolithic thing. It’s a stack, and the pieces are separable:

This matters for Kubernetes specifically. Kubernetes talks to a runtime through the Container Runtime Interface (CRI). It historically bridged to Docker through a shim, but that shim (dockershim) was removed in Kubernetes 1.24 (2022). Modern clusters run containerd or CRI-O directly — the Docker daemon isn’t installed on your nodes. The images you build with Docker still run there, because they’re OCI images. (That is the headline surprise for people arriving at Kubernetes from Docker, so it’s worth internalising now.)

Rootless containers

By default the Docker daemon runs as root, which means a flaw in the daemon or a container escape becomes a host-root problem. Rootless mode runs the whole stack as an unprivileged user by leaning on the user namespace: UID 0 inside the container maps to your ordinary UID outside, so “root in the container” has no real power on the host. Podman does this by default; Docker supports it via a rootless setup. It’s a major hardening step for multi-tenant or untrusted workloads, at the cost of some networking and performance caveats.

Digests and true immutability

A tag is a movable label; a digest is not. An image digest is the sha256 hash of the image’s manifest, for example:

ghcr.io/acme/web@sha256:3f9a...c21

Because it’s derived from the content itself, a digest names exact bytes — change one file and the digest changes. Pinning by digest gives you a cryptographic guarantee: the image you deploy is byte-for-byte the one you tested, and no one can slip new content under the same reference (a real supply-chain concern). Kubernetes records the digest it actually pulled, and production GitOps pipelines increasingly deploy by digest, not tag. docker pull prints the digest; docker images --digests shows it locally.

Why “it works on my machine” finally dies

The oldest bug in software is the environment mismatch: different library versions, a missing system package, a different locale, a config file only you have. Containers kill it by shipping the environment with the app. The image contains the exact userland — the libraries, the interpreter, the files — so the process runs against the same filesystem everywhere.

What the image does not contain is the kernel (that’s shared) — so the one class of “works on my machine” that survives is a kernel or architecture mismatch: an arm64 image won’t run on amd64 without emulation, and a workload depending on a brand-new kernel feature can still differ across hosts. That’s why images are built per-architecture, and multi-arch manifests exist to bundle several under one tag. For the everyday “but it ran on my laptop!” though, containers really are the cure.

Practice challenges

Work through these in order — each builds on the last. Try it yourself first; the solution follows. (You’ll need Docker or Podman; everything is free and local, and it helps to have completed the Hands-on lab so the kv-web project and the nginx:1.27-alpine base image are already present.)

Challenge 1 — Run and identify (Beginner). Run the tiny hello-world image in the foreground so it prints and exits, then prove the stopped container still exists afterwards.

Solution.

docker run hello-world
docker ps -a          # -a shows stopped containers too, not just running ones

Without -d, docker run runs in the foreground; hello-world prints its message and exits. Plain docker ps would show nothing (the container has stopped) — docker ps -a reveals it. Why it matters: a container that exits is stopped, not deleted — it lingers until you docker rm it.

Challenge 2 — Build with a tag and read the size (Beginner). Using the lab’s kv-web project, build the image as kv-web:practice and display just that image and its size.

Solution.

docker build -t kv-web:practice .
docker images kv-web:practice

-t names and tags the image. Two tags on the same content share layers, so adding :practice costs almost no extra disk. Why: an image is identified by repository:tag, and tags are cheap labels over the same layers.

Challenge 3 — Read the layers and find yours (Intermediate). Show the layer history of kv-web:practice and identify which single layer you added versus the base image’s layers.

Solution.

docker history kv-web:practice

The top row is your COPY index.html … layer (a few hundred bytes); every <missing> row beneath it is a base-image layer with no local build record. Why: an image is an ordered stack of layers, and docker history prints it newest-on-top.

Challenge 4 — Prove ephemerality / copy-on-write (Intermediate). Start a container, write a file inside it, remove the container, start a fresh one from the same image, and show the file is gone.

Solution.

docker run -d --name c1 kv-web:practice
docker exec c1 sh -c 'echo hi > /tmp/scratch && cat /tmp/scratch'   # -> hi
docker rm -f c1
docker run -d --name c2 kv-web:practice
docker exec c2 ls /tmp/scratch    # -> ls: /tmp/scratch: No such file or directory
docker rm -f c2

The file lived only in c1’s writable layer; removing the container discarded it. Why: the image is immutable and per-container writes are copy-on-write and ephemeral — exactly why durable state belongs in volumes or databases.

Challenge 5 — Pin by digest (Advanced). The nginx:1.27-alpine base is already on your machine from the lab. Print its digest, and explain what pinning to nginx@sha256:… guarantees that the tag nginx:1.27-alpine does not.

Solution.

docker images --digests nginx
# REPOSITORY  TAG           DIGEST              IMAGE ID       ...
# nginx       1.27-alpine   sha256:abc123...    a1b2c3d4e5f6   ...

A tag is a movable label — 1.27-alpine can be rebuilt and re-pushed, so the same reference can yield different bytes over time. A digest is the sha256 of the image manifest, so nginx@sha256:abc123… names exact, unchangeable bytes: the image you deploy is provably the one you tested. Trade-off: digests are unreadable and you lose automatic patch updates — so teams often pin digests in production manifests while tracking friendlier tags in development. (A purely local, never-pushed image may not have a registry digest yet; digests are assigned on push/pull.)

Common mistakes & troubleshooting

Symptom Likely cause Fix
docker: Cannot connect to the Docker daemon Docker Desktop / the daemon isn’t running Start Docker Desktop (or sudo systemctl start docker on Linux) and retry
port is already allocated on docker run -p 8080:80 Something already uses host port 8080 Pick another host port, e.g. -p 8081:80, or stop the other process
Edited index.html but the page didn’t change The running container still uses the old image Rebuild (docker build) and recreate the container (docker rm -f then docker run) — running containers don’t auto-update
Every build reinstalls dependencies (slow) COPY . . placed before the install step, busting the cache Copy dependency manifests first, RUN the install, then COPY the source last
Image is surprisingly large Heavy base image, or files copied in that aren’t needed Use a -slim/alpine/distroless base and add a .dockerignore
denied: requested access on push Not logged in, or wrong repository/registry name docker login <registry> and verify the registry/repo:tag reference
latest pulled something unexpected latest is just the default tag and can move Pin a real version tag (1.4.2) or a digest (@sha256:...)

Common beginner mistakes

These aren’t error messages — they’re misconceptions, the wrong mental models that quietly cause the errors above. Naming them fixes the thinking, not just the symptom.

Best practices

Security notes

Quick check

  1. In one sentence, what’s the core difference between a container and a virtual machine?
  2. What’s the difference between an image and a container?
  3. In the reference ghcr.io/acme/web:1.4.2, name each of the three parts.
  4. Why does putting COPY . . before RUN npm ci slow your builds down?
  5. What does the tag latest actually mean, and why shouldn’t you depend on it in production?

Answers

  1. A VM virtualizes hardware and runs its own full OS/kernel; a container virtualizes the OS and shares the host kernel, isolated by namespaces and limited by cgroups — making it far smaller and faster to start.
  2. An image is a read-only template (a blueprint); a container is a running instance created from that image, with its own thin writable layer. One image, many containers.
  3. ghcr.io is the registry, acme/web is the repository (name/namespace), and 1.4.2 is the tag (the version label).
  4. Because each instruction is a cached layer that’s invalidated when it or anything before it changes. Copying source first means any code change busts the cache for the npm ci step below it, forcing a full dependency reinstall every build. Copy manifests first, install, then copy source.
  5. latest is simply the default tag used when you don’t specify one — not a guarantee of “newest” or “stable.” It can be re-pointed to different content, so builds become non-reproducible; pin a real version tag or a digest instead.

Exercise

Take a tiny program in any language you like (a one-file Python, Node, or Go “hello” works well) and containerize it from scratch:

  1. Write a Dockerfile that starts from an appropriate official base image, copies your file in, and sets a CMD to run it.
  2. Build it as myapp:1.0 and run it.
  3. Run docker history myapp:1.0 and identify which layer is yours versus the base image’s.
  4. Make a deliberate one-line change to your source and rebuild. Note in docker history/build output which layers were CACHED and which were rebuilt — and confirm the ordering matched your expectation.
  5. Then reorder the Dockerfile to copy dependency files before source (if your language has dependencies), rebuild twice, and observe the cache behaviour improve.
  6. Clean up everything (docker rm -f, docker rmi, docker system prune -f) and confirm with docker images and docker ps that nothing is left.

Bonus: replace your base image with a -slim or alpine variant and compare docker images sizes before and after.

Interview questions

Q: What’s the difference between a container and a VM, and when would you still choose a VM? A: A VM virtualizes hardware and runs a full guest OS with its own kernel via a hypervisor — strong isolation, but heavy (GBs, slow boot). A container shares the host kernel and is isolated by namespaces/cgroups — lightweight (MBs, instant start) and dense. You’d still choose a VM when you need a different OS kernel, the strongest isolation boundary (e.g. running untrusted multi-tenant code without a sandboxed runtime), or kernel-level features a container can’t get.

Q: Explain the difference between an image and a container. A: An image is an immutable, read-only template — a stack of filesystem layers plus metadata. A container is a runtime instance of an image with an added writable layer and a running process. You can start many containers from one image; the image is the class, the container is the object.

Q: What is an image layer, and why does it matter for build performance? A: Each Dockerfile instruction creates a read-only layer that’s a diff over the previous one; an image is an ordered stack of these layers. Layers are cached and shared. Build performance depends on ordering: Docker reuses cached layers until the first changed instruction, then rebuilds it and everything after. Putting stable steps (base, dependency install) early and volatile steps (source copy) late maximizes cache hits.

Q: Why is relying on the latest tag considered an anti-pattern? A: latest is just the default tag, not a stable channel — it can be moved to point at different image content, so the “same” reference can yield different bytes over time. That breaks reproducibility and makes rollbacks unreliable. Pin explicit version tags, and pin by digest (@sha256:...) when you need guaranteed immutability.

Q: What’s a container registry, and what kinds have you used? A: A registry stores and distributes images; you push to it and pull from it. Public ones like Docker Hub host base/official images; private ones like Amazon ECR, Azure ACR, Google Artifact Registry, and GHCR hold an organization’s own images near where they run. Access can be public or authenticated, and rate limits (e.g. Docker Hub anonymous pulls) can affect deployments.

Q: How do you keep an image small and secure? A: Start from a minimal, official, version-pinned base (-slim/alpine/distroless); use multi-stage builds so build tools don’t ship to runtime; add a .dockerignore; run as a non-root USER; never bake secrets into layers; and scan the image for vulnerabilities before shipping.

Q: Docker, containerd, runc, CRI — how do they relate, and does Kubernetes use Docker? A: runc is the low-level OCI runtime that actually creates namespaces/cgroups and starts the process; containerd is the daemon that manages images and container lifecycle and calls runc; Docker is a developer-facing toolkit built on top of containerd. Kubernetes talks to a runtime through the CRI and today uses containerd or CRI-O directly — the Docker shim (dockershim) was removed in Kubernetes 1.24. Images built with Docker still run fine because they’re OCI images.

Certification mapping

This lesson maps to the KCNA (Kubernetes and Cloud Native Associate) exam — the entry-level, multiple-choice certification that’s the natural first goal for this course.

Glossary

Next steps

You can now package and run software as containers — the unit Kubernetes is built to orchestrate. Next, meet the system that runs these images across many machines:

Related reading on KloudVin:

DockerContainersImagesRegistriesKubernetes
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments