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.
Namespace
What It Isolates
Container Sees
PID
Process IDs
Its own PID 1, host sees different PID
NET
Network stack
Own eth0, routing table, iptables
MNT
Filesystem mounts
Own /, /proc, /sys view
UTS
Hostname + domain
Can set own hostname
IPC
POSIX message queues, SysV IPC
Isolated IPC resources
USER
UID/GID mapping
Container root → host unprivileged UID
TIME
System 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)FROMnode:18AS builder ← ~800MBWORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production (tiny nginx image)FROMnginx:alpine← ~7MBCOPY --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 / 9READY
Press "step →" to walk through docker run internals, or "▶ play all" to see the full sequence.
# 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 bridge2. 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:804. MASQUERADE rule for outbound (container → internet):
MASQUERADE all -- 172.17.0.0/16 anywhere
Mode
How
Performance
Use Case
bridge
Virtual veth + docker0 bridge
~5% overhead
Default — most containers
host
Shares host network stack
Zero overhead
High-performance, trusted workloads
none
No networking
—
Batch jobs, no network needed
overlay
VXLAN tunnel between hosts
~10% overhead
Multi-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
Command
What Happens Internally
docker build
Each instruction: spawn temp container → run command → snapshot diff → new layer. Metadata instructions update config.json only.
docker pull
Downloads image manifest JSON from registry. Checks which layer SHAs are missing locally. Downloads only missing layer tarballs in parallel.
docker push
Uploads manifest + any layers registry doesn't already have (checked by SHA256). Shared layers across images are never duplicated.
Creates a NEW process in the container's EXISTING namespaces. Uses nsenter internally. Container PID 1 is not affected.
docker stop
Sends SIGTERM to PID 1 inside container. Waits 10s (configurable). If still running, sends SIGKILL. runc waits for process exit, then tears down namespaces.
docker rm
Removes container metadata. Deletes the upperdir (writable layer). Lower image layers are untouched and still shared.
docker rmi
Removes image layers from /var/lib/docker/overlay2/ — but ONLY if no container (running or stopped) references them.