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?
python3 --version # my Mac
docker run --rm python:3.13-slim python --version # a container on the same MacPython 3.14.0 Python 3.13.16
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:
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 containerhello-from-a hello-from-b cat: can't open '/note.txt': No such file or directory
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:
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)"'
doneAlpine 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
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:
docker build -t hello-app .
docker run --rm hello-apphello from Python 3.13.16
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.