Demystifying Docker

Containers vs VMs

Virtual machines

Physical hardware
CPU, RAM, disk, NIC
Host OS / Kernel
Host kernel
Hypervisor
Virtualises hardware for guests (each VM)
VM 1
Virtual hardware
OS / kernel
Processes / apps
VM 2
Virtual hardware
OS / kernel
Processes / apps
VM 3
Virtual hardware
OS / kernel
Processes / apps

Containers (Docker)

Physical hardware
CPU, RAM, disk, NIC
Host OS / Kernel
Single shared kernel
Docker
Daemon, image layers, runtime
Container A
Process(es)
Filesystem isolation
Network isolation
Container B
Process(es)
Filesystem isolation
Network isolation
Container C
Process(es)
Filesystem isolation
Network isolation

Linux filesystem (primer)

Linux filesystem and permissions (primer)

Paths start at / (root). Each file has permissions (owner/group/others) and ownership. Docker uses this when setting up container roots and volume mounts.

File tree with permissions
d rwx r-x r-x = dir owner group others
/
root of filesystem
drwxr-xr-x root:root
home/
user home dirs
drwxr-xr-x root:root
etc/
config files
drwxr-xr-x root:root
var/
variable data (logs, caches)
drwxr-xr-x root:root
tmp/
temporary, world-writable
drwxrwxrwx root:root
app/
your app (common in containers)
drwxr-xr-x myappuser:myappuser
r read w write x execute (dirs: enter)

Linux mounts (primer)

Linux mounts (primer)

Mount points attach filesystems into the tree; the kernel's mount table records what is mounted where. Bind mount = same filesystem at another path.

Mount table (conceptual)
path ← mounted from (source)
/
rootfs (e.g. ext4 on sda1)
/home
ext4 on sda2
/mnt/backup
ext4 on sdb1
/mnt/data
bind from /data (same filesystem, other path)

chroot

Host filesystem
/
├── home/
├── etc/
├── var/
│   └── lib/
│       └── docker/
│           └── containers/
│               └── abc123…/  ← container root
│                   ├── app/
│                   ├── etc/
│                   └── bin/
What the process sees (inside container)
/  ← container root
├── app/
├── etc/
└── bin/

Mount namespace

Host mount table
/ — rootfs
/home — disk partition
/var/lib/docker/... — volumes, overlay
Container mount table (isolated)
/ — overlay (image + RW layer)
/app/data — volume (bind or named)

PID namespace

Host view
PID 1 — init
PID 100 — systemd
PID 200 — sshd
PID 5000 — dockerd
PID 5001 — app (container)
PID 5002 — worker
PID 5003 — worker
Container view (same processes)
PID 1 — app
PID 2 — worker
PID 3 — worker

Docker images — layers

Command 1 of 8 · 1 layerTotal: 37 MB
FROMalpine:3.19
RUNapk add --no-cache nodejs
ENVNODE_ENV=production
WORKDIR/app
COPYpackage*.json ./
RUNnpm ci --omit=dev
COPY. .
CMD["node", "server.js"]
sha256:00007bedb7f337 MB

Only FROM, RUN, COPY, and ADD create layers. ENV, WORKDIR, CMD, etc. set metadata only.

Layer cache reuse

Command 1 of 8 · 1 layer · start from layer 3Total: 37 MB
FROMalpine:3.19cached
RUNapk add --no-cache nodejscached
ENVNODE_ENV=productioncached
WORKDIR/srcrebuilt
COPYpackage*.json ./rebuilt
RUNnpm ci --omit=devrebuilt
COPY. .rebuilt
CMD["node", "server.js"]rebuilt
sha256:00007bedb7f337 MBcached

Changing WORKDIR from /app to /src invalidates cache at that line. Commands before: cache hit. From layer 3 onward: rebuilt.

FROM instruction

Step 1 of 8 · 0 layersTotal: 0 B
FROMalpine:3.19
COPYpackage*.json ./
RUNnpm ci
Step through: FROM pulls in the base image's layers, then COPY and RUN add layers on top.

FROM doesn't add one layer—it attaches your image to the base image's entire layer stack. Your RUN/COPY/ADD layers sit on top.

Starting a container — layers merge

0 layers applied
sha256:00004f9b1c9324 MB
sha256:0000b106979534 MB
sha256:00002746267f13 MB
sha256:00007bedb7f337 MB
(empty)

Each layer (bottom to top) adds or changes files. The union is the read-only filesystem for the container.

Container from image

Image only (read-only layers)
Image filesystem (read-only)
📁/
📁app
📄index.js
📄package.json
📁etc
📄config.json
📁usr
📁bin
📄node
Container filesystem (read+write union)
📁/
📁app
📄index.js
📄package.json
📁etc
📄config.json
📁usr
📁bin
📄node

Start container to see the writable layer. Stop freezes the view; Restart clears and returns to image only.

Volume mapping

Step 1 of 3: No volume
Command
docker run --rm my-image
Host
(no volume; container uses R/W layer only)
Container
/app/data/
📄config.json
📄state.json
(ephemeral; lost on rm)

Without a volume, writes in the container are only in the R/W layer and disappear when the container is removed.

User mapping

Host user list (examples)

0root
1daemon
1000alice
1001bob

No user mapping (default)

Container runs as
root (UID 0)
→ Host sees: root (0)
Container runs as
appuser (UID 1000)
→ Host sees: alice (1000)
Same UID; names can differ (container "appuser" ≠ host "alice").

With user mapping (user namespace)

Example mapping (e.g. alice:100000:65536 in /etc/subuid)
Container 0 → Host 100000
Container 1 → Host 100001
Container 1000 → Host 101000
Container runs as
root (UID 0)
→ Host sees: 100000 (unprivileged)
Container runs as
UID 1000
→ Host sees: 101000

Linux network stack

Linux network stack — how the pieces fit together

A packet flows through the stack. Incoming: interface → routing → iptables → app. Outgoing: app → (DNS for hostnames) → routing → iptables → interface.

Application binds :8080, sockets iptables / nftables NAT, filter Routing which interface? eth0 192.168.1.10 lo 127.0.0.1 DNS "postgres" → IP

Network namespace

Host
eth0 (physical)
docker0 (bridge)
veth0abc ←→ container's eth0
Container
lo (127.0.0.1)
eth0 (e.g. 172.17.0.2)

Port mapping

Step 1 of 5: packet path
host eth0dst host:8080
packet
iptablesDNAT
host:8080 → 172.17.0.2:8080
docker0 + vethforward
container eth0172.17.0.2:8080
app :8080delivered

Packet arrives on host eth0 with destination host:8080.

Networking between containers

Step 1 of 3: Layout
Container networking: DNS via loopback, then data packet through bridgeContainer AHost (bridge + veth)Container BNode serverlo (127.0.0.11)Docker DNSA eth0172.18.0.2vethbridgevethPostgresB eth0

Node server (A) and Postgres (B) each have their own network namespace with lo and eth0. The bridge and veth pairs live on the host.

Host network mode

Bridge vs host: packets move faster with fewer steps
Bridge mode (7 hops) vs Host mode (3 hops) packet path comparisonBridgeClienteth0iptablesdocker0vetheth0appHostClienteth0app

Both animations run together. Host mode packet (blue) reaches the app in half the time—fewer hops, no iptables, bridge, or veth.