Docker Containers vs Virtual Machines: Images and Layers Explained
DevOpsLesson 1 of 45 min read
What you'll learn
- Explain how a container differs from a virtual machine, including what they share and what they isolate
- Distinguish an image, a layer, a container and a registry tag
- Run, list, stop and remove containers with the core docker commands
- Prove to yourself that a container's writable layer is disposable
On this page
A bakery's label printer runs a small Python script that reads orders and prints allergen stickers. It works on the owner's laptop. On the new tablet-sized PC at the shop it crashes on the first line, because that machine has an older Python and a missing font library. Nobody changed the script. The environment around it differs, and the script quietly depended on that environment. Containers exist to take the environment out of the "it depends" column.
What a container actually is
A container is an ordinary process (or a small group of them) that the Linux kernel has been told to treat specially. Namespaces give it its own view of process IDs, network interfaces, mount points and hostnames, so from the inside it looks like a private machine. Control groups (cgroups) cap how much CPU and memory it may consume. There is no second operating system booting up. The container shares the host's kernel and simply gets a different filesystem and a restricted view of everything else.
A virtual machine takes the opposite route. A hypervisor emulates hardware, and each VM boots its own complete kernel and operating system on top of that fake hardware. That costs memory and boot time, but it gives a harder isolation boundary, because a guest kernel bug does not automatically touch the host kernel.
| Aspect | Virtual machine | Container |
|---|---|---|
| Kernel | Own guest kernel per VM | Shares the host kernel |
| Starts in | Typically tens of seconds (a full boot) | Typically a fraction of a second (a process start) |
| Disk footprint | Whole OS image, often gigabytes | App plus its userland libraries, often tens to hundreds of megabytes |
| Isolation strength | Strong (hardware-level boundary) | Weaker (kernel is shared), hardened with extra controls |
| Good fit | Different operating systems, untrusted tenants | Many identical, short-lived app instances |
Images, layers and the registry
A container starts from an image, a read-only template holding a filesystem snapshot plus metadata such as the default command. Images are built from layers. Each instruction that changes the filesystem in a Dockerfile adds one layer, which records only what changed relative to the layer beneath it. Layers are identified by content hashes, so two images that begin with the same base share those layers on disk and over the network. Pulling a second Python app does not download Python again.
When you start a container, Docker stacks the image's read-only layers and adds one thin writable layer on top. Anything the process writes lands there. Delete the container and that layer goes with it. A registry (Docker Hub, GitHub Container Registry, a cloud provider's registry) stores images under names like python:3.12-slim, where the part after the colon is the tag. A tag is a movable label, not a guarantee, which matters later.
# Pull an image, then run a throwaway container from it
docker pull python:3.12-slim
docker run --rm python:3.12-slim python --version
# List local images and running containers
docker image ls
docker ps
# Include stopped containers too
docker ps -aOutput
Python 3.12.x
The patch number printed will depend on when the image was last published. --rm tells Docker to delete the container as soon as the process exits, which keeps docker ps -a from filling up with leftovers during experiments.
See the layers yourself
Reading about layers is less convincing than looking at them. This Dockerfile belongs to the bakery's label script. Every instruction below creates a layer except WORKDIR and CMD, which only change metadata or create an empty directory.
FROM python:3.12-slim
WORKDIR /srv/labels
RUN pip install --no-cache-dir reportlab
COPY labels.py .
CMD ["python", "labels.py"]docker build -t bakery-labels:1 .
docker history bakery-labels:1Output
IMAGE CREATED CREATED BY SIZE <id> 1 minute ago CMD ["python" "labels.py"] 0B <id> 1 minute ago COPY labels.py . # buildkit <small> <id> 1 minute ago RUN pip install --no-cache-dir reportlab ... <medium> <id> 1 minute ago WORKDIR /srv/labels 0B <id> 2 weeks ago (the layers of python:3.12-slim follow) ...
The output above is illustrative: IDs, ages and sizes will be different on your machine, and the exact columns vary by Docker version. What stays true is the shape. Your four instructions sit on top of the base image's layers, and the pip install layer is the heavy one while the COPY layer is tiny. That observation drives the whole next lesson.
The writable layer is disposable
Run this three-line experiment once and the idea sticks. The first container writes a file and exits. The second container, started from the same image, goes looking for it.
docker run --name scratch python:3.12-slim sh -c "echo parcel-17 > /tmp/note.txt && cat /tmp/note.txt"
docker rm scratch
docker run --rm python:3.12-slim cat /tmp/note.txtOutput
parcel-17 cat: /tmp/note.txt: No such file or directory
The file lived in the first container's writable layer and disappeared with docker rm. Containers are meant to be replaceable; anything that must survive belongs in a volume, which is covered in Docker Volumes, Networks and Environment Config.
Common mistakes and the symptoms they cause
- Keeping data inside the container. Symptom: the database is empty after the container is recreated. Fix: use a named volume for anything that must persist.
- Relying on the `latest` tag. Symptom: the same
docker rungives different behaviour next month, or CI breaks overnight with no code change. Fix: pin a specific version tag. - Pulling the wrong CPU architecture. Symptom:
exec format errorwhen an image built for amd64 meets an arm64 host, or a warning that the requested image's platform does not match the detected host platform. Fix: use a multi-architecture image or pass--platform. - Reusing a container name. Symptom:
Conflict. The container name is already in use. Fix:docker rmthe old one or use--rmfor throwaway runs. - Treating a container as a VM and SSH-ing in to patch it. Symptom: fixes vanish on the next deploy and no two instances match. Fix: change the Dockerfile and rebuild.
How this shows up in real projects and interviews
The classic interview question is "how is a container different from a VM?" A strong answer mentions the shared kernel, namespaces and cgroups, and is honest that the isolation boundary is weaker. Follow-ups usually probe layers ("why does changing one line rebuild everything below it?") and the difference between an image and a container. On the job, the first thing you will do with this knowledge is read someone else's Dockerfile and docker history to find out why an image is 1.4 GB.
Try it yourself
Pick any small script you own and write a Dockerfile for it that starts from a slim base image. Build it, then run docker history on the result and identify which layer is the largest and which instruction created it. Finally, run a container from it that creates a file, remove the container, and confirm the file is gone in a fresh one.
Show a hint
Use docker build -t myscript:1 . and docker run --rm myscript:1 ls /. If your base image has no ls, try a slim Debian-based tag.
Key takeaways
- A container is an isolated process sharing the host kernel, not a miniature operating system.
- An image is a stack of read-only layers; a container adds one disposable writable layer on top.
- Layers are content-addressed, so identical base layers are stored and downloaded only once.
- Tags are movable labels: pin versions instead of trusting
latest. - Data you care about must live outside the container's writable layer.
Test yourself
A trick from this lesson's ride: A VM brings its own engine; a container borrows the host's engine and gets a private garage. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is the difference between a Docker image and a container?
An image is a read-only template made of layers, plus metadata such as the default command. A container is a running (or stopped) instance created from that image, with its own thin writable layer. You can start many containers from one image.
Is Docker the same as a virtual machine?
No. A VM emulates hardware and boots its own kernel, while a Docker container is an isolated process that shares the host's kernel. Containers start faster and use less memory, but their isolation boundary is weaker than a VM's.
Why do Docker images have layers?
Layers let Docker reuse work. Each Dockerfile instruction that changes files creates a layer identified by its content hash, so unchanged layers are cached during builds and shared between images, which saves time, disk space and download bandwidth.