Docker Basics #1: What Is a Container — VM vs. Docker Ecosystem

9 min read

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 .dockerignore and 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.

VM vs. container
        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:

ImageContainer
AnalogyClass / blueprintInstance / actual thing
StateRead-only, never changesRunning, stopped, deletable
How it’s madedocker build (from a Dockerfile)docker run (from an image)
Where it livesHost 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.

Pieces that make up Docker
                ┌────────────────────────────┐
                │       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 like containerd, 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.yaml file boots up web + db + cache together. (Topic of the intermediate series.)
  • Docker Hub / GHCR / ECRregistries 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.

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:

Ubuntu — official install script
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER  # so you can run docker without sudo

After usermod, log out and back in once for the group change to take effect.

When that’s done, check the version:

Version check
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:

hello-world
docker run hello-world

The first run prints something like this:

Output
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:

  1. The docker CLI asked the daemon, “run the hello-world image.”
  2. The daemon checked its local cache → not there.
  3. It pulled hello-world:latest from Docker Hub (Pulling from library/hello-world).
  4. Created a container from the pulled image and ran it.
  5. The program inside the container printed “Hello from Docker!” and exited.
  6. The container is stopped but not deleted (still on disk).

Let’s look at what just got created:

Inspect images and containers
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_curie
  • docker images shows images cached on the host disk.
  • docker ps shows only running containers; docker ps -a includes the stopped ones.

To clean up:

Cleanup
docker rm bold_curie         # delete the container (use your own name)
docker rmi hello-world       # delete the image

One 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.

Step into an Ubuntu container
docker run -it ubuntu:24.04 bash

The first run pulls the Ubuntu 24.04 image (~80MB), and you immediately get a shell prompt inside the container.

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:/# exit

One 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.

CommandWhat 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 -aRunning / all containers
docker imagesCached images
docker logs <container>Container’s stdout/stderr
docker exec -it <container> bashDrop 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-world and docker 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.

X