Docker is not magic — it's a well-engineered userspace tool that orchestrates Linux kernel primitives you already know: namespaces (Lab 16), cgroups (Lab 17), and union filesystems. In this lab you'll install Docker inside a container, examine the overlay2 filesystem, trace the runc/containerd/dockerd call chain, inspect container networking internals, and understand exactly what happens when you run docker run.
💡 OCI = Open Container Initiative. Both the image format and runtime spec are OCI standards. This means you can use runc directly with any OCI-compliant image, bypassing Docker entirely.
💡 You can safely delete all Docker state with systemctl stop docker && rm -rf /var/lib/docker — but you'll lose all images, containers, and volumes!
Step 3: overlay2 — Union Filesystem Layers
overlay2 stacks read-only layers (from the image) with a read-write layer (the container):
📸 Verified Output:
📸 Verified Output:
📸 Verified Output:
📸 Verified Output:
💡 This is copy-on-write (CoW). When a container modifies a file from the image layer, the kernel copies the original to UpperDir first, then modifies the copy. The original image layer is never touched.
Step 4: Inspect Container Internals with docker inspect
📸 Verified Output (key fields):
📸 Verified Output:
💡 NanoCpus: 500000000 = 0.5 CPUs. Docker uses nanosecond CPU units internally, which map to cpu.max = "50000 100000" in the cgroup.
Step 5: Container Networking — veth Pairs and the docker0 Bridge
📸 Verified Output:
📸 Verified Output:
The @if14 and @if13 notation shows the peer interface index — they form a virtual Ethernet cable between the host bridge and the container namespace.
💡 Interface index binding: In the output above, interface 13 (container's eth0) is the peer of interface 14 (host's veth3a4b5c). When a packet leaves the container, it travels through this virtual cable to the bridge, then to the host's network.
Step 6: Image Layers and Multi-Stage Build Internals
📸 Verified Output:
📸 Verified Output:
💡 Cache invalidation: Any change to a layer invalidates all subsequent layers. This is why you should: (1) put COPY after RUN apt-get install, and (2) use --mount=type=cache in BuildKit for package manager caches.
Step 7: What runc Actually Does — OCI Bundle
📸 Verified Output:
💡 containerd-shim: After runc starts the container and exits, the containerd-shim-runc-v2 process stays alive to: (1) keep stdin/stdout pipes open, (2) report exit status, and (3) allow containerd to restart without killing containers.
Step 8: Capstone — Trace a docker run from syscall to process
Scenario: A junior engineer asks: "What EXACTLY happens when I run docker run nginx?" Trace the complete path.
# Pull an image and examine its layers
docker pull alpine:latest 2>/dev/null
# Inspect image layers
docker history alpine:latest
IMAGE CREATED CREATED BY SIZE COMMENT
a606584aa9aa 3 weeks ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0
<missing> 3 weeks ago ADD alpine-minirootfs.tar.gz / #… 8.83MB buildkit.dockerfile.v0
# Show the actual layer directories
docker inspect alpine:latest --format '{{json .RootFS.Layers}}' | python3 -m json.tool
# Examine the overlay2 storage
ls /var/lib/docker/overlay2/
# Each layer has this structure:
# LAYER_ID/
# diff/ ← actual filesystem changes for this layer
# lower ← colon-separated list of lower layer IDs (parent chain)
# merged/ ← union mount (only exists when container is running)
# work/ ← overlay2 work directory (required by kernel)
# link ← short symlink ID
LAYER_ID=$(ls /var/lib/docker/overlay2/ | grep -v l | head -1)
echo "Layer: $LAYER_ID"
ls /var/lib/docker/overlay2/$LAYER_ID/
cat /var/lib/docker/overlay2/$LAYER_ID/link 2>/dev/null
Layer: a3b4c5d6e7f8...
diff link
MLKJIHG
# When you run a container, Docker creates an ADDITIONAL layer on top:
docker run --name testbox -d alpine sleep 60
CID=$(docker ps -q)
# Find the container's overlay2 directory
docker inspect testbox --format '{{.GraphDriver.Data.MergedDir}}'
docker inspect testbox --format '{{.GraphDriver.Data.UpperDir}}' # The writable layer
docker inspect testbox --format '{{.GraphDriver.Data.LowerDir}}' # Read-only image layers
apt-get install -y -qq iproute2 bridge-utils
# Show the docker0 bridge
ip link show docker0
ip addr show docker0
brctl show docker0 2>/dev/null || bridge link show
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
link/ether 02:42:e3:f1:28:5c brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
docker0 8000.0242e3f1285c no veth3a4b5c
# For each running container, there's a veth pair:
# - vethXXXXXX on the HOST side (attached to docker0 bridge)
# - eth0 inside the CONTAINER namespace
# List all veth interfaces
ip link show type veth
# Find the container's PID and enter its network namespace
CID=$(docker ps -q -f name=demo)
PID=$(docker inspect $CID --format '{{.State.Pid}}')
echo "Container PID: $PID"
# Look at host-side veth
ip link show | grep veth
# Enter container network namespace and see container's interface
nsenter -t $PID -n ip addr
nsenter -t $PID -n ip route
Container PID: 12345
14: veth3a4b5c@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
13: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2
# Build a multi-stage image to see layer caching
cat > /tmp/Dockerfile << 'EOF'
# Stage 1: Builder
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y gcc
WORKDIR /app
COPY . .
RUN echo '#include <stdio.h>\nint main(){printf("Hello\\n");}' > hello.c && gcc -o hello hello.c
# Stage 2: Final (only copies binary — no compiler!)
FROM ubuntu:22.04
COPY --from=builder /app/hello /usr/local/bin/hello
CMD ["/usr/local/bin/hello"]
EOF
cd /tmp && docker build -t hello-multi . 2>&1 | head -20
docker history hello-multi
[+] Building 45.3s (10/10) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 378B
=> [builder 1/4] FROM ubuntu:22.04
=> [builder 2/4] RUN apt-get update && apt-get install -y gcc
=> [builder 3/4] WORKDIR /app
=> [builder 4/4] RUN echo '...' > hello.c && gcc -o hello hello.c
=> [stage-1 1/2] FROM ubuntu:22.04
=> [stage-1 2/2] COPY --from=builder /app/hello /usr/local/bin/hello
=> exporting to image
IMAGE CREATED CREATED BY SIZE
a1b2c3d4e5f6 2 seconds ago CMD ["/usr/local/bin/hello"] 0B
<missing> 5 seconds ago COPY /app/hello /usr/local/bin/… 16.4kB
<missing> 2 weeks ago ...ubuntu base layers...
# Layer caching: build again — all layers should be CACHED
docker build -t hello-multi . 2>&1 | grep -E "CACHED|FROM"
# The config.json is what Docker generates and passes to runc
# You can see the actual config for a running container:
docker inspect demo --format '{{.HostConfig.SecurityOpt}}'
cat /var/run/docker/runtime-runc/moby/$(docker ps -q -f name=demo)/state.json 2>/dev/null | python3 -m json.tool | head -20