You've learned what containers are and why they work. Now let's install the tooling and run your very first container — then dissect exactly what happened.
1. What Gets Installed
When you "install Docker," you're actually installing a stack of cooperating components:
┌──────────────────────────────────────────────────────┐ │ Your Machine (Host OS) │ │ │ │ ┌─────────────┐ ┌──────────────────────┐ │ │ │ docker CLI │────────▶│ dockerd (daemon) │ │ │ └─────────────┘ REST │ • image management │ │ │ API │ • networking │ │ │ ┌─────────────┐ │ • volumes │ │ │ │ Docker │ └──────────┬───────────┘ │ │ │ Compose │ │ │ │ └─────────────┘ ▼ │ │ ┌──────────────────────┐ │ │ │ containerd │ │ │ │ (container runtime) │ │ │ └──────────┬───────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────┐ │ │ │ runc │ │ │ │ (OCI runtime — does │ │ │ │ the actual clone/ │ │ │ │ unshare syscalls) │ │ │ └──────────────────────┘ │ └──────────────────────────────────────────────────────┘
| Component | Role |
|---|---|
docker CLI | The command you type. Talks to the daemon via REST API over a Unix socket. |
dockerd | Long-running daemon. Manages images, networks, volumes. Delegates container execution to containerd. |
containerd | Industry-standard container runtime. Pulls images, manages container lifecycle. |
runc | Low-level OCI runtime. Makes the actual Linux syscalls (clone, unshare, pivot_root) you learned about in Lesson 3. |
| Docker Compose | Multi-container orchestration tool. Bundled with Docker Desktop; separate install on Linux. |
Docker Desktop vs Docker Engine
- Docker Desktop (Mac/Windows) — includes a Linux VM, the daemon, CLI, Compose, a GUI, and Kubernetes. One installer, everything works.
- Docker Engine (Linux) — just the daemon + CLI + containerd + runc. No VM needed because you're already on Linux.
2. Installation (Quick Reference)
You likely already have Docker installed. If not, here's the fast path:
Linux (Ubuntu/Debian)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in for group to take effect
macOS
# Download Docker Desktop from https://docker.com/products/docker-desktop
# Or via Homebrew:
brew install --cask docker
Windows
# Download Docker Desktop from https://docker.com/products/docker-desktop
# Requires WSL 2 backend (enabled during install)
Verify your installation:
$ docker --version
Docker version 24.0.7, build afdd53b
$ docker compose version
Docker Compose version v2.23.0
Installation guides go stale fast. What matters is understanding the components you just installed — that knowledge applies whether you use Docker 20.x or 27.x.
3. Your First Run: hello-world
$ docker run hello-world
This one command triggers a surprisingly complex flow. Let's trace every step:
You type: docker run hello-world
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. CLI parses command, sends request to dockerd │
│ via /var/run/docker.sock │
└─────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. dockerd checks: do I have image "hello-world:latest" │
│ locally? → NO │
└─────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. dockerd contacts Docker Hub (registry.docker.io) │
│ • Downloads image manifest (SHA, layers) │
│ • Downloads layers (just one tiny layer for hello-world) │
│ • Stores in /var/lib/docker/overlay2/ │
└─────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. dockerd tells containerd: "create a container from │
│ this image" │
│ • containerd creates filesystem snapshot (overlay) │
│ • containerd calls runc │
└─────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. runc creates namespaces + cgroups, runs the binary │
│ inside the container → prints "Hello from Docker!" │
└─────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. Process exits (code 0). Container stops. │
│ Container still exists (status: Exited) until removed. │
└─────────────────────────────────────────────────────────────┘
docker run hello-world.Expected output:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
c1ec31eb5944: Pull complete
Digest: sha256:d211f485f2dd1dee407a80973c8f129f00d54604...
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
...
4. Pulling an Image
docker pull is the explicit version of the implicit pull that happens during docker run. Here's what it actually does:
- Contact the registry — resolve
ubuntu→registry.docker.io/library/ubuntu:latest - Download the manifest — a JSON file listing all layers (by SHA-256 digest) and the image config
- Download layers — each layer is a compressed tar; only layers you don't already have are downloaded
- Store locally — layers go into
/var/lib/docker/overlay2/; the image metadata is indexed
$ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
aece8493d397: Pull complete
Digest: sha256:2b7412e6465c3c7fc5bb21...
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
Inspect what you have locally:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest ca2b0f26964c 2 weeks ago 77.9MB
hello-world latest d2c94e258dcb 9 months ago 13.3kB
| Column | Meaning |
|---|---|
| REPOSITORY | Image name (from the registry) |
| TAG | Version label (latest is the default) |
| IMAGE ID | First 12 chars of the SHA-256 content hash |
| SIZE | Disk space used (shared layers aren't double-counted) |
5. Running an Interactive Container
$ docker run -it ubuntu bash
Flags: -i = keep STDIN open, -t = allocate a pseudo-TTY. Together they give you an interactive shell.
You're now inside a container. Let's look around:
root@3f7a2b1c9e4d:/# hostname
3f7a2b1c9e4d
root@3f7a2b1c9e4d:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4624 3720 pts/0 Ss 12:00 0:00 bash
root 10 0.0 0.0 7060 1564 pts/0 R+ 12:00 0:00 ps aux
root@3f7a2b1c9e4d:/# ls /
bin boot dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
root@3f7a2b1c9e4d:/# cat /etc/os-release | head -2
PRETTY_NAME="Ubuntu 22.04.3 LTS"
NAME="Ubuntu"
root@3f7a2b1c9e4d:/# exit
$
What's different from the host?
- Hostname — a random hex string (the container ID), not your machine's name
- Process list — only
bashand its children. PID 1 is bash, not systemd. - Filesystem — Ubuntu's root filesystem, regardless of what OS your host runs
- Users — you're root inside, but that doesn't mean root on the host (user namespace)
- Network — separate network namespace with its own IP address
Any files you create inside the container are lost when you remove it. The image stays unchanged. This is the "cattle not pets" model — containers are disposable.
6. Container Lifecycle Commands
| Command | What it does |
|---|---|
docker run <image> | Create + start a new container from an image |
docker ps | List running containers (-a includes stopped ones) |
docker stop <id> | Send SIGTERM, then SIGKILL after timeout |
docker start <id> | Restart a stopped container (same filesystem state) |
docker rm <id> | Delete a stopped container permanently |
docker logs <id> | Show stdout/stderr captured from the container |
A container's lifecycle:
Created → Running → Stopped (Exited) → Removed
↑ │
└───────────┘ (docker start)
Hands-On Tasks
- Run:
docker run hello-world - Read the output carefully — it explains the steps Docker took.
- Run:
docker ps -a— find the stopped hello-world container. - Clean up:
docker rm <container-id>
- Run:
docker run -it ubuntu bash - Inside, try:
hostname,ps aux,ls /,whoami - Create a file:
echo "hello" > /tmp/test.txt - Type
exitto leave - Run a new container:
docker run -it ubuntu bash— check if/tmp/test.txtexists (it won't!) - Exit and clean up:
docker rm $(docker ps -aq)
- Run:
docker run -d -p 8080:80 --name my-nginx nginx - Visit
http://localhost:8080in your browser — you should see "Welcome to nginx!" - Check logs:
docker logs my-nginx - Check running containers:
docker ps - Stop it:
docker stop my-nginx - Remove it:
docker rm my-nginx
Key flags: -d = detached (run in background), -p 8080:80 = map host port 8080 to container port 80, --name = give it a human-friendly name.
Knowledge Check
When you run docker run hello-world and the image isn't local, what happens first?
Which component makes the actual Linux syscalls (clone, unshare) to create namespaces?
You run docker run -it ubuntu bash, create a file, then exit. You run the same command again. Is the file there?
Podman is a drop-in replacement for Docker. Same commands, no daemon required:
podman run hello-world
podman run -it ubuntu bash
podman run -d -p 8080:80 nginx
The architecture differs (no central daemon — each container is a child process), but the CLI and image format are identical. Everything you learn here applies to both.
🔑 Key Takeaways
- Docker is a stack: CLI → dockerd → containerd → runc. Each layer has a distinct job.
docker run= pull + create + start. It's a compound operation.- Images are downloaded as layers from a registry. Shared layers are stored once.
- Containers are ephemeral. Each
runcreates a fresh instance. Changes don't persist unless you use volumes (Lesson 7). - Interactive (
-it) vs detached (-d) — two modes for two use cases: debugging vs running services. - Lifecycle: run → stop → start → rm. Stopped containers still exist until explicitly removed.