Docker Basics #1: What Is a Container — VM vs. Docker Ecosystem
The backend tracks on this blog (FastAPI, Django, Go in Practice) all hit the same question in their final post — “how do I deploy this?”. The answer was always Docker. So I broke Docker out into its own track of 4 series, 24 posts.
This series is Docker Basics, 6 posts.
- #1 What is a container — VM vs. Docker ecosystem ← this post
- #2 Writing your first Dockerfile — RUN, COPY, CMD
- #3 Images and containers — build, run, ps, logs, exec
- #4 Volumes and networks
- #5 Registries — Docker Hub, GHCR, push/pull
- #6
.dockerignoreand the build context
This post pins down two things — what a container is and what bundle of tools Docker actually is. We finish by running docker run hello-world for real.
“Works on my machine” #
It’s the first thing you hear when you start talking about deploys. The app runs fine on the developer’s machine and breaks on the server. Dig into the cause and almost every time it’s the same picture:
- The OS is different (macOS / Ubuntu / Amazon Linux)
- Python / Node / Go versions are different
- System libraries (
libssl,libpq,imagemagick) are different - Environment variables, file permissions, time zones — all the small stuff is different
You need a way to bundle “the app + everything it depends on” into a single unit and run it identically anywhere. That way is the container.
How is it different from a virtual machine? #
The tool that solved this same problem before containers was the virtual machine (VM) — VirtualBox, VMware, cloud VMs like EC2. Both “run apps in an isolated environment,” but the layer being isolated is different.
Virtual Machine Container
┌─────────────────────────┐ ┌─────────────────────────┐
│ App A │ App B │ │ App A │ App B │
├────────────┼────────────┤ ├────────────┼────────────┤
│ Bin/Lib │ Bin/Lib │ │ Bin/Lib │ Bin/Lib │
├────────────┼────────────┤ ├────────────┴────────────┤
│ Guest OS │ Guest OS │ │ Docker Engine │
├────────────┴────────────┤ ├─────────────────────────┤
│ Hypervisor │ │ Host OS │
├─────────────────────────┤ ├─────────────────────────┤
│ Host OS │ │ Hardware │
├─────────────────────────┤ └─────────────────────────┘
│ Hardware │
└─────────────────────────┘- A VM runs a whole guest OS on top of a hypervisor — kernel and all. Boot takes minutes, and a blank Ubuntu box already eats a few GB of disk and a few hundred MB of RAM by itself.
- A container shares the host’s kernel. Isolation comes from Linux kernel features (namespaces, cgroups). So one container is essentially “an isolated process” — milliseconds to start, almost no memory overhead.
Because they’re light, one host can run dozens or hundreds of containers, and the build/deploy cycle gets dramatically shorter. The one constraint — you can’t use a different kernel from the host. When you use Docker on macOS or Windows, a small Linux VM is installed under the hood, and containers run on top of that. (That’s what Docker Desktop actually is.)
Where did containers come from? #
Docker didn’t invent the container. The Linux kernel has had isolation primitives for a long time:
- chroot (1979) — change a process’s root directory to isolate the filesystem
- cgroups (2007) — limit CPU, memory, and IO per group
- namespaces (2002~) — isolate PID, network, mount, user, etc. per process
- LXC (2008) — the first container tool that bundled the above
These primitives were powerful but hard to use. There was no standard way to package an app into a container. Every person and every company did it differently.
When Docker arrived in 2013, that was what got cleaned up:
- Dockerfile — a unified build definition
- Image — a portable artifact
- Registry — a way to share images
- Docker CLI — one tool to drive all of it
Containers are old tech, but the wrapping paper and shipping protocol is what Docker brought. It’s now standardized as OCI (Open Container Initiative), and other implementations like podman, containerd, and Kubernetes’ cri-o all share the same image format.
Image vs. container — they aren’t the same thing #
The two words trip people up early. Short version:
| Image | Container | |
|---|---|---|
| Analogy | Class / blueprint | Instance / actual thing |
| State | Read-only, never changes | Running, stopped, deletable |
| How it’s made | docker build (from a Dockerfile) | docker run (from an image) |
| Where it lives | Host disk and registries (Hub, GHCR, ECR …) | Host disk |
You can spin up 100 containers from the same image, and those 100 don’t affect each other. The fact that the image doesn’t change is what gives the whole system its reproducibility.
A tour of the Docker ecosystem #
“Docker” actually refers to several tools sitting under one name. Sketching the big picture before you start using them makes the rest of the series easier to follow.
┌────────────────────────────┐
│ Docker CLI │ ← the commands you type
│ (docker run / build ...) │
└─────────────┬──────────────┘
│ REST API
┌─────────────▼──────────────┐
│ Docker daemon │ ← the process doing the work
│ (dockerd → containerd) │
└──┬──────┬────────┬─────────┘
│ │ │
┌──────▼─┐ ┌─▼────┐ ┌▼────────────┐
│ Images │ │ Net │ │ Containers │
│ (cache)│ │ Vol │ │ (runtime) │
└────────┘ └──────┘ └─────────────┘- Docker Engine — the daemon (
dockerd) that actually starts containers and uses lower-level runtimes likecontainerd, plus the CLI (docker) that drives it. - Docker CLI — commands like
docker run,docker build,docker ps. Under the hood they’re just thin clients sending REST API requests to the daemon. - Dockerfile — a text file describing “how to build an image.” (We tackle this in #2.)
- Docker Compose — a tool to define and run multiple containers as one unit. One
compose.yamlfile boots up web + db + cache together. (Topic of the intermediate series.) - Docker Hub / GHCR / ECR — registries that store and share images. Same relationship as GitHub and GitHub Container Registry. (Covered in #5.)
- Docker Desktop — the integrated package for macOS / Windows. It bundles a small Linux VM and the tools above into a single install.
This series covers CLI + Dockerfile + registry — the pieces you need to build and run a single container well. Compose and operations are for the next series.
Install — Docker Desktop #
For macOS / Windows users, installing Docker Desktop once is all it takes.
- Download: docker.com/products/docker-desktop
- On macOS, pick the build for Apple Silicon or Intel depending on your chip.
Once Docker Desktop is running, the docker command works in the terminal immediately. No PATH setup needed.
On Linux (Ubuntu) you install the Engine directly instead of Desktop:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # so you can run docker without sudoAfter usermod, log out and back in once for the group change to take effect.
When that’s done, check the version:
docker --version
# Docker version 27.x.x, build ...
docker info
# daemon status, container count, image count, storage driver, etc.If docker info errors out, the daemon isn’t running. On macOS / Windows make sure Docker Desktop is up; on Linux check sudo systemctl start docker.
First container — hello-world
#
There’s a traditional command for confirming Docker is set up properly:
docker run hello-worldThe first run prints something like this:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
...
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
...Behind the one-line command, here’s what happened:
- The
dockerCLI asked the daemon, “run thehello-worldimage.” - The daemon checked its local cache → not there.
- It pulled
hello-world:latestfrom Docker Hub (Pulling from library/hello-world). - Created a container from the pulled image and ran it.
- The program inside the container printed “Hello from Docker!” and exited.
- The container is stopped but not deleted (still on disk).
Let’s look at what just got created:
docker images
# REPOSITORY TAG IMAGE ID SIZE
# hello-world latest abc123... 13.3kB
docker ps -a
# CONTAINER ID IMAGE COMMAND STATUS NAMES
# d3f4... hello-world "/hello" Exited (0) 10 seconds ago bold_curiedocker imagesshows images cached on the host disk.docker psshows only running containers;docker ps -aincludes the stopped ones.
To clean up:
docker rm bold_curie # delete the container (use your own name)
docker rmi hello-world # delete the imageOne step further — docker run -it ubuntu
#
hello-world prints one line and exits, so it’s hard to feel anything. Stepping inside an actual Linux container is when “wait, this is what was so light?” sinks in.
docker run -it ubuntu:24.04 bashThe first run pulls the Ubuntu 24.04 image (~80MB), and you immediately get a shell prompt inside the container.
root@a1b2c3d4e5f6:/# cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04 LTS"
...
root@a1b2c3d4e5f6:/# uname -r
6.10.0-... # the kernel version the container is sharing
root@a1b2c3d4e5f6:/# exitOne interesting fact — cat /etc/os-release says Ubuntu, but uname -r reports the kernel the container is sharing. On native Linux that’s the host kernel; on Docker Desktop it may be the kernel of Docker Desktop’s internal Linux VM. That’s where the “containers share the host kernel” detail surfaces.
The -it flags do two things:
-i(--interactive): keep the container’s stdin open (so it can take input)-t(--tty): attach a pseudo-TTY (so the shell behaves naturally)
For interactive containers they almost always travel together. When you exit, the shell terminates, and since the shell is the container’s main process, the container stops too.
Common commands at a glance #
A reference table for commands that show up across the series. No need to memorize — come back when you get stuck.
| Command | What it does |
|---|---|
docker run <image> | Create and run a container from an image |
docker run -it <image> <cmd> | Interactive mode (often used to drop into a shell) |
docker run -d <image> | Run in the background (detached) |
docker ps / docker ps -a | Running / all containers |
docker images | Cached images |
docker logs <container> | Container’s stdout/stderr |
docker exec -it <container> bash | Drop into a shell inside a running container |
docker stop <container> | Graceful stop (SIGTERM → SIGKILL) |
docker rm <container> | Delete a stopped container |
docker rmi <image> | Delete an image |
docker build -t <name> . | Build an image from the Dockerfile in the current directory |
docker pull <image> | Pull an image from a registry |
docker push <image> | Push an image to a registry |
Wrap-up #
The picture from this post:
- A container is a portable unit that bundles an app and everything it depends on. It solves “works on my machine.”
- A VM virtualizes the guest OS too; a container shares the host kernel for lighter isolation.
- Containers stand on old kernel primitives — chroot, cgroups, namespaces. What Docker did was build a standard build and shipping protocol on top.
- Image = an unchanging blueprint; container = a running instance built from it.
- Docker isn’t one tool — it’s an ecosystem of Engine + CLI + Compose + Hub. This series covers CLI + Dockerfile + registry.
- We ran our first containers with
docker run hello-worldanddocker run -it ubuntu.
In the next post (#2 Writing your first Dockerfile) we move past pulling someone else’s image — to building an image for your own app. We’ll write the simplest possible Dockerfile with FROM, RUN, COPY, CMD, and walk through docker build baking it into an image.