This is the first post in our Docker fundamentals series. Later posts cover production Dockerfiles, networking, Compose, and image security.
"A container is like a lightweight virtual machine" is the analogy almost everyone hears first, and it's the reason so many people get confused six months later when a container behaves in a way no VM ever would. It's worth building the correct mental model from the start instead.
What a container actually is
A container is an isolated process running on the same kernel as the host machine — not a separate operating system, not a hypervisor-managed guest. Linux provides two kernel features that make this isolation possible: namespaces (giving a process its own view of processes, network interfaces, and filesystem mounts) and cgroups (limiting how much CPU, memory, and I/O that process can consume). Docker is, underneath all its tooling, a friendly interface over those two kernel primitives.
This is why containers start in milliseconds while VMs take seconds to minutes — there's no separate kernel to boot. It's also why a container sharing the host's kernel means a kernel-level vulnerability can, in the worst case, be exploited across container boundaries in a way that's architecturally impossible with a real VM's hardware-enforced isolation. The trade-off is real in both directions, not just a speed win.
Images vs. containers
An image is a read-only template — filesystem contents plus metadata (what command to run, what ports to expose). A container is a running instance of an image, with a thin writable layer on top.
docker pull nginx:1.27
docker run -d --name my-nginx -p 8080:80 nginx:1.27docker run creates a container from the nginx:1.27 image and starts it. Critically, docker run again creates a second, independent container from the same image:
docker run -d --name my-nginx-2 -p 8081:80 nginx:1.27Two containers, one shared underlying image (Docker only stores the image's layers once, regardless of how many containers use it), each with their own independent writable layer and their own isolated process. This is the relationship most people get wrong at first: the image is the recipe, the container is one specific instance of a meal made from it — you can make many meals from the same recipe without changing the recipe itself.
Layers, and why they matter for more than just disk space
An image is built from a stack of read-only layers, each one representing a single instruction in the Dockerfile that created it.
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl
COPY app.py /app/app.py
CMD ["python3", "/app/app.py"]Each of FROM, RUN, and COPY produces its own layer, stacked on top of the previous one. Docker caches layers by content hash — rebuild this image without changing app.py, and the RUN apt-get install layer (usually the slowest step) is reused unchanged from cache, rather than re-executed:
Step 2/4 : RUN apt-get update && apt-get install -y curl
---> Using cache
---> a1b2c3d4e5f6
This caching behavior is exactly why instruction order in a Dockerfile is a real performance decision, not a stylistic one — a detail we go into in depth in the next post in this series, because getting it wrong is one of the most common reasons a team's Docker builds feel unnecessarily slow.
The container lifecycle
docker ps # running containers
docker ps -a # all containers, including stopped ones
docker stop my-nginx # sends SIGTERM, then SIGKILL after a grace period
docker start my-nginx # starts an existing (stopped) container again
docker rm my-nginx # permanently removes a stopped container
docker logs my-nginx # stdout/stderr from the container's main process
docker exec -it my-nginx sh # a shell inside a running container, for debuggingTwo of these deserve emphasis because they're a common source of confusion:
stopandrmare different operations. A stopped container still exists — its filesystem changes, its logs, its configuration — until yourmit. This is genuinely useful for debugging: a container that crashed can be inspected withdocker logseven after it's stopped.docker execruns a new process inside an already-running container's namespaces — it doesn't attach to the container's original process. Killing yourexecshell doesn't affect the container's actual main process at all.
Why containers are disposable, on purpose
The writable layer created by docker run is deleted along with the container when you rm it. Any file written inside a running container that isn't in a mounted volume disappears the moment that container is removed. This is a deliberate design choice, not an oversight: containers are meant to be treated as disposable and stateless, with anything that needs to persist — a database's actual data, user-uploaded files — stored outside the container in a volume or bind mount.
docker run -d --name my-db -v pgdata:/var/lib/postgresql/data postgres:16pgdata here is a named volume, managed by Docker and stored outside any single container's writable layer. Remove and recreate the container, and the volume — and the real data inside it — survives, because it was never actually inside the container's disposable filesystem layer to begin with.
What to actually remember from this post
- A container is an isolated process sharing the host kernel, not a separate virtual machine — namespaces and cgroups provide the isolation, not a hypervisor.
- An image is a read-only template; a container is a running instance of one — many containers, one shared image.
- Layers are cached by content, which is what makes Dockerfile instruction order a genuine performance lever, not just style.
- Containers are disposable by design — anything that needs to survive a container being removed belongs in a volume, not the container's own writable layer.
Next in the series: Writing Production-Ready Dockerfiles, where layer caching and multi-stage builds turn this basic understanding into images that are actually fast to build and safe to ship.
