01

Containers vs Virtual Machines

Virtual Machine
App A
Guest OS (Ubuntu 22.04)
Hypervisor (VMware/VBox)
Host OS + Hardware
  • Full OS per VM (1–10 GB each)
  • Minutes to start (BIOS + OS boot)
  • Hypervisor overhead on CPU/RAM
  • Strong isolation (separate kernel)
Container
App A
Docker / OCI Runtime (runc)
Host OS Kernel (shared)
  • Shared host kernel (MB per image)
  • Milliseconds to start (just a process)
  • Near-zero overhead
  • Isolation via namespaces + cgroups
DOCKER ON MAC AND WINDOWS

Linux containers need a Linux kernel. Docker Desktop on Mac or Windows secretly runs a lightweight Linux VM (LinuxKit on Mac using the Virtualization framework). Your containers run inside this hidden VM, not directly on macOS. This is why Docker Desktop on Mac is slightly slower than Docker on Linux — there's a VM boundary. Docker Desktop handles this transparently.

02

The Linux Kernel Primitives

Containers aren't a kernel feature — they're an orchestration of existing kernel features. Docker uses two Linux mechanisms: namespaces for isolation and cgroups for resource limits.

Namespaces — Isolation

A namespace wraps a global system resource so that processes inside the namespace see their own isolated instance. Docker creates a new set of namespaces for each container.

NamespaceWhat It IsolatesContainer Sees
PIDProcess IDsIts own PID 1, host sees different PID
NETNetwork stackOwn eth0, routing table, iptables
MNTFilesystem mountsOwn /, /proc, /sys view
UTSHostname + domainCan set own hostname
IPCPOSIX message queues, SysV IPCIsolated IPC resources
USERUID/GID mappingContainer root → host unprivileged UID
TIMESystem clock (Linux 5.6+)Can have different system time
Inspect a container's namespaces
# Get the PID of a running container on the host $ docker inspect nginx_container --format '{{.State.Pid}}' 12345 # See its namespace file descriptors $ ls -la /proc/12345/ns/ lrwxrwxrwx ipc -> ipc:[4026532205] lrwxrwxrwx mnt -> mnt:[4026532203] lrwxrwxrwx net -> net:[4026532208] lrwxrwxrwx pid -> pid:[4026532206] lrwxrwxrwx user -> user:[4026531837] ← same as host (not isolated) lrwxrwxrwx uts -> uts:[4026532204] # Enter the container's network namespace from the host $ sudo nsenter -t 12345 -n ip addr

cgroups — Resource Limits

Control Groups limit how much CPU, memory, disk I/O, and network bandwidth a container can use. Without cgroups, a container could starve other containers or the host.

Docker → cgroup mapping
# What Docker flags become under the hood: docker run --memory=256m --cpus=0.5 nginx # Creates cgroup at: /sys/fs/cgroup/docker/abc123.../ memory.limit_in_bytes = 268435456 ← 256MB cpu.cfs_quota_us = 50000 ← 50ms per 100ms period = 0.5 CPU cpu.cfs_period_us = 100000 # Check live memory usage: $ cat /sys/fs/cgroup/docker/abc123/memory.usage_in_bytes 134217728 ← 128MB currently used
03

Docker Images and OverlayFS

A Docker image is a stack of read-only layers. Each layer is a tar archive of filesystem changes — added, modified, or deleted files — from one Dockerfile instruction. Layers are content-addressed by SHA256 hash and shared across images.

OverlayFS: How Layers Merge

OverlayFS Mount
# OverlayFS combines multiple directories into one view mount -t overlay overlay \ -o lowerdir=/var/lib/docker/overlay2/layer3:/var/lib/docker/overlay2/layer2:/var/lib/docker/overlay2/layer1, \ upperdir=/var/lib/docker/overlay2/CONTAINER_ID/diff, \ workdir=/var/lib/docker/overlay2/CONTAINER_ID/work \ /var/lib/docker/overlay2/CONTAINER_ID/merged ╔═══════════════════════════════════════════════════╗ ║ upperdir (read-write container layer) ║ ← writes go here ╠═══════════════════════════════════════════════════╣ ║ layer3 /etc/nginx/nginx.conf (COPY) [RO] ║ ╠═══════════════════════════════════════════════════╣ ║ layer2 /usr/sbin/nginx (RUN apt install) [RO] ║ ╠═══════════════════════════════════════════════════╣ ║ layer1 ubuntu:22.04 base OS [RO] ║ ╚═══════════════════════════════════════════════════╝ ↓ merged view (what container sees) / ← looks like a normal Linux filesystem
COPY-ON-WRITE (CoW)

When a container reads a file, it reads from whatever layer contains the file — fast, no copying. When a container writes to a file that exists in a lower layer (read-only), the kernel copies the file up to the upperdir first, then modifies the copy. Subsequent reads of that file hit the upperdir. This is transparent to the process inside the container.

Interactive Layer Stack

Click each layer to see what Dockerfile instruction created it and which files it contains.

IMAGE LAYER EXPLORER — nginx:alpine example
Top = newest layer. Bottom = base image.
OverlayFS merge
SELECT A LAYER
Click any layer on the left to inspect it.
04

docker build — What Happens for Each Instruction

FROM

FROM ubuntu:22.04

Docker pulls the base image from the registry (if not cached locally). Unpacks all its layers into the overlay lowerdir stack. This becomes the starting point for the new image.

# Pulls and unpacks layers to: /var/lib/docker/overlay2/sha256:e3a6... / (77MB)
RUN

RUN apt-get update && apt-get install nginx

Docker creates a temporary container from the current layers, executes the command inside it, then takes a snapshot of the filesystem diff. That diff becomes a new read-only layer. The temporary container is discarded. This layer records every file apt-get added or modified.

COPY

COPY nginx.conf /etc/nginx/

Files are copied from the build context (your local filesystem) into a new layer. The layer is a tar with just those files at their target paths. If the source files haven't changed since the last build, the cache hit is used — no new layer needed.

META

ENV, EXPOSE, CMD, ENTRYPOINT

These instructions don't create filesystem layers. They're stored in the image's config.json — a JSON file describing environment variables, exposed ports, the command to run on docker run. No bytes added to the union filesystem.

Build Cache Strategy

CACHE INVALIDATION RULE

Cache is invalidated at the first instruction that changes — and every subsequent instruction rebuilds from scratch. This is why Dockerfile ordering matters for build speed:

❌ SLOW (cache miss on COPY)
COPY . . ← copies ALL files RUN npm install ← rebuilds every time
✅ FAST (cache hits npm install)
COPY package*.json . ← only package.json RUN npm install ← cached unless deps change COPY . . ← rest of source

Multi-Stage Builds

Multi-stage: 800MB → 25MB
# Stage 1: Build (full Node.js toolchain) FROM node:18 AS builder ← ~800MB WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # Stage 2: Production (tiny nginx image) FROM nginx:alpine ← ~7MB COPY --from=builder /app/dist /usr/share/nginx/html ← Final image: ~25MB ← node_modules never shipped!
05

docker run — Animated Sequence

Step through exactly what happens internally when you type docker run nginx. From CLI to running process in ~200ms.

Step 0 / 9 READY
CLI DOCKERD CONTAINERD RUNC CONTAINER docker run nginxvia /var/run/docker.sock gRPC: CreateContainer run container bundle unshare() namespacesPID+NET+MNT+UTS+IPC mount OverlayFS + cgroups exec nginx (PID 1 in container) Container running! Port 80 available CONTAINER RUNNING ✓
Press "step →" to walk through docker run internals, or "▶ play all" to see the full sequence.

The Architecture Stack

Docker component hierarchy
Docker CLI ↓ HTTP/REST via Unix socket (/var/run/docker.sock) dockerd (Docker Daemon) ↓ gRPC containerd (runtime supervisor) ↓ protobuf containerd-shim-runc-v2 (process supervisor, survives dockerd restart) ↓ runc (OCI runtime — creates namespaces, mounts, execs) ↓ Your Process (nginx, node, python — PID 1 inside container) # Kubernetes uses containerd directly (dockershim removed in k8s 1.24)
06

Docker Networking

Bridge Network (Default)

How -p 8080:80 actually works
# docker run -p 8080:80 nginx # Docker creates: 1. Virtual ethernet pair (veth): eth0 (inside container) ← one end veth3b2d1c (on host) ← other end, attached to docker0 bridge 2. docker0 bridge interface on host (172.17.0.1/16) Container gets 172.17.0.2 3. iptables rule for port mapping: $ iptables -t nat -L DOCKER DNAT tcp -- anywhere anywhere tcp dpt:8080 to:172.17.0.2:80 ← forward host:8080 to container:80 4. MASQUERADE rule for outbound (container → internet): MASQUERADE all -- 172.17.0.0/16 anywhere
ModeHowPerformanceUse Case
bridgeVirtual veth + docker0 bridge~5% overheadDefault — most containers
hostShares host network stackZero overheadHigh-performance, trusted workloads
noneNo networkingBatch jobs, no network needed
overlayVXLAN tunnel between hosts~10% overheadMulti-host (Swarm, Kubernetes)
07

Container Security

SHARED KERNEL = SHARED RISK

All containers share the host Linux kernel. A kernel vulnerability (privilege escalation exploit) affects all containers. This is fundamentally different from VMs where a hypervisor escape is needed. Use gVisor or Kata Containers for stronger isolation in high-security environments.

Run as Non-Root
# In Dockerfile: RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser ← never run as root # Or at runtime: docker run --user 1000:1000 nginx
Capabilities
# Drop ALL, add only what's needed docker run --cap-drop=ALL \ --cap-add=NET_BIND_SERVICE \ nginx # Never use --privileged in prod! # It gives container near-root host access
08

Commands Cheat Sheet — What They Actually Do

CommandWhat Happens Internally
docker buildEach instruction: spawn temp container → run command → snapshot diff → new layer. Metadata instructions update config.json only.
docker pullDownloads image manifest JSON from registry. Checks which layer SHAs are missing locally. Downloads only missing layer tarballs in parallel.
docker pushUploads manifest + any layers registry doesn't already have (checked by SHA256). Shared layers across images are never duplicated.
docker runCLI → dockerd (socket) → containerd (gRPC) → runc: create namespaces, mount OverlayFS, setup cgroups, setup veth, exec entrypoint.
docker execCreates a NEW process in the container's EXISTING namespaces. Uses nsenter internally. Container PID 1 is not affected.
docker stopSends SIGTERM to PID 1 inside container. Waits 10s (configurable). If still running, sends SIGKILL. runc waits for process exit, then tears down namespaces.
docker rmRemoves container metadata. Deletes the upperdir (writable layer). Lower image layers are untouched and still shared.
docker rmiRemoves image layers from /var/lib/docker/overlay2/ — but ONLY if no container (running or stopped) references them.
END OF HOW-06