Containers from zero

What a container is, why it exists, and how it differs from an image and from a VM. Four small experiments you can run in five minutes.

Part 1 of 2 · Containers from zero4 min read
python 3.14works python 3.9 why doesn’t it work here?

If you’ve never used Docker, start with one sentence:

A container is your app plus everything it needs, running as an isolated process.

The rest of this post unpacks that sentence with four experiments. I ran them on my Mac with Docker Desktop 4.54 (Engine 29.1.2). Every number below is real output, and you can reproduce all of it with the commands shown.

1. Why containers exist

Your app doesn’t run alone. It needs a language runtime (Python, Java, Node), libraries, and sometimes system packages. On your laptop they’re all installed and working. On the server, they’re a different version, or missing. That’s the whole “it works on my machine” problem.

Here’s the problem on one machine. Before you look at the output, guess.

PredictMy Mac has Python 3.14. What does the second command print?

Run it yourself
python3 --version                                  # my Mac
docker run --rm python:3.13-slim python --version  # a container on the same Mac
Python 3.14.0
Python 3.13.16
Same laptop, two different Pythons. Your first line will differ; the second will not.

Same laptop, two different Pythons. The container didn’t use mine. It used the one packed inside the image. That’s the point: the image carries the runtime, the libraries and your code together. Any machine with Docker runs the same thing. The server no longer needs the right Python. It needs Docker.

2. Image vs container

These two words get mixed up all the time, so here’s the difference.

  • An image is a read-only template: files plus a command to start.
  • A container is a running copy of an image.

If you program, think class and object. Or recipe and dish: one recipe, as many dishes as you like.

To see it, I started three containers from the same alpine image, and each one wrote a different note:

Run it yourself
for n in a b c; do
docker run -d --name demo-$n alpine sh -c "echo hello-from-$n > /note.txt; sleep 300"
done
docker exec demo-a cat /note.txt     # hello-from-a
docker exec demo-b cat /note.txt     # hello-from-b
docker run --rm alpine cat /note.txt # a brand-new container
hello-from-a
hello-from-b
cat: can't open '/note.txt': No such file or directory
alpine image: read-only, shared by all four hello-a hello-b hello-c no /note.txt

One image: read-only, shared by every container.

Each container saw only its own note, and a fresh container saw none. Every container gets its own thin writable layer on top of the image. The image itself never changes.

That has a practical consequence. Anything a container writes is gone when you delete the container. Data you want to keep goes in a volume, which is a topic for another post.

3. Many distros, one kernel

“Isn’t a container just a small VM?” Let’s ask three containers which operating system they are, and which kernel they run on:

Run it yourself
for img in alpine python:3.13-slim ubuntu:24.04; do
docker run --rm $img sh -c '. /etc/os-release; echo "$PRETTY_NAME | $(uname -r)"'
done
Alpine Linux v3.24 | 6.12.54-linuxkit
Debian GNU/Linux 13 (trixie) | 6.12.54-linuxkit
Ubuntu 24.04.5 LTS | 6.12.54-linuxkit
alpine debian ubuntu one kernel: 6.12.54-linuxkit three distros are only files
Three distributions are three sets of files on one shared kernel.

Three different distributions, one kernel. The “distro” inside a container is only files: a package manager, a shell, libraries. The kernel, the core of the operating system that actually runs processes, is shared by every container on the machine.

That’s the real difference from a virtual machine. A VM boots a whole operating system with its own kernel. A container borrows the host’s kernel and only gets its own view of files, processes and network. That’s why a container starts in a fraction of a second and an image can be tiny: alpine is 13.6 MB.

On a Mac there’s one twist. macOS isn’t Linux, so Docker Desktop runs a single small Linux VM, and all containers share its kernel. That’s where linuxkit comes from. You get one VM for the whole machine, not one per container.

4. From Dockerfile to a running app

You describe an image with a Dockerfile. The smallest useful one is three lines:

FROM python:3.13-slim
COPY app.py .
CMD ["python", "app.py"]
  • FROM: start from an existing image (here, Python on Debian).
  • COPY: add your file to it.
  • CMD: the command to run when a container starts.

With an app.py that prints its Python version:

Run it yourself
docker build -t hello-app .
docker run --rm hello-app
hello from Python 3.13.16
python:3.13 app.pyrequests all of it, in one box
The image carries the app, its libraries and the runtime in one box.

docker build took 0.5 s and docker run about 0.3 s on my machine. That’s because the 204 MB base image was already downloaded. The first time, Docker pulls it once, and every later build reuses it.

Build once, run anywhere Docker runs.

The whole idea, in four lines

  • A container exists so your app ships with its runtime and libraries.
  • An image is the read-only template, and a container is a running copy with its own writable layer.
  • Containers share the host’s kernel. That’s what makes them light, and what makes them different from VMs.
  • A Dockerfile turns “how to set up my app” into a file you can build and run anywhere.

In part 2 we go one level deeper. Why does one changed line sometimes rebuild for minutes? Why does docker stop take 10 seconds? And why does a container die with exit code 137 and no error in the logs?

Measured 2026-10-16 on Docker Desktop 4.54.0, Engine 29.1.2. Get the files: Dockerfile and app.py, and the raw output.

New posts by email

One email when a post ships, about twice a month. Nothing else, and one click to leave.

Latest: A container is a process · RSS