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)   │   │
│                          └──────────────────────┘   │
└──────────────────────────────────────────────────────┘
Fig 1. Components installed with Docker and how they communicate.
Component Role
docker CLIThe command you type. Talks to the daemon via REST API over a Unix socket.
dockerdLong-running daemon. Manages images, networks, volumes. Delegates container execution to containerd.
containerdIndustry-standard container runtime. Pulls images, manages container lifecycle.
runcLow-level OCI runtime. Makes the actual Linux syscalls (clone, unshare, pivot_root) you learned about in Lesson 3.
Docker ComposeMulti-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
Focus on what, not how

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.   │
 └─────────────────────────────────────────────────────────────┘
Fig 2. The complete lifecycle of 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:

  1. Contact the registry — resolve ubunturegistry.docker.io/library/ubuntu:latest
  2. Download the manifest — a JSON file listing all layers (by SHA-256 digest) and the image config
  3. Download layers — each layer is a compressed tar; only layers you don't already have are downloaded
  4. 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
REPOSITORYImage name (from the registry)
TAGVersion label (latest is the default)
IMAGE IDFirst 12 chars of the SHA-256 content hash
SIZEDisk 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 bash and 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
Ephemeral by default

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 psList 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

Task 1: Run hello-world and read the output
  1. Run: docker run hello-world
  2. Read the output carefully — it explains the steps Docker took.
  3. Run: docker ps -a — find the stopped hello-world container.
  4. Clean up: docker rm <container-id>
Task 2: Explore an interactive Ubuntu container
  1. Run: docker run -it ubuntu bash
  2. Inside, try: hostname, ps aux, ls /, whoami
  3. Create a file: echo "hello" > /tmp/test.txt
  4. Type exit to leave
  5. Run a new container: docker run -it ubuntu bash — check if /tmp/test.txt exists (it won't!)
  6. Exit and clean up: docker rm $(docker ps -aq)
Task 3: Run nginx in detached mode
  1. Run: docker run -d -p 8080:80 --name my-nginx nginx
  2. Visit http://localhost:8080 in your browser — you should see "Welcome to nginx!"
  3. Check logs: docker logs my-nginx
  4. Check running containers: docker ps
  5. Stop it: docker stop my-nginx
  6. 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?

  • runc creates a new namespace
  • containerd starts the container
  • The daemon pulls the image from Docker Hub
  • The CLI compiles the image from source

Which component makes the actual Linux syscalls (clone, unshare) to create namespaces?

  • docker CLI
  • dockerd
  • containerd
  • runc

You run docker run -it ubuntu bash, create a file, then exit. You run the same command again. Is the file there?

  • No — each docker run creates a brand new container
  • Yes — the file persists in the image
  • Yes — Docker automatically mounts a volume
  • It depends on the filesystem driver
What if you use Podman?

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 run creates 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.