πŸ“š Learning Hub
Β· 8 min read
Last updated on

How Docker Containers Work β€” and Why AI Workloads Stress the Boundaries


Every Docker tutorial starts the same way: β€œContainers are like lightweight VMs.” That’s a useful lie to get started, but it falls apart the moment you need to debug a networking issue, understand why your image is 2 GB, or figure out why a process inside a container can still see the host’s clock.

Containers are not VMs. They’re isolated processes running on the host kernel, constrained by a handful of Linux primitives. If you’ve ever wondered what Docker actually is, this post tears the lid off the engine.

Containers vs VMs β€” The Real Difference

A virtual machine runs a full guest operating system on emulated hardware, managed by a hypervisor. Every VM carries its own kernel, its own init system, its own memory management.

A container is a regular Linux process (or group of processes) that the kernel has been told to isolate and constrain. There is no guest kernel. There is no hypervisor. The container shares the host’s kernel directly.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Virtual Machines                    β”‚
β”‚                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚  App A   β”‚  β”‚  App B   β”‚  β”‚  App C   β”‚      β”‚
β”‚  β”‚  Bins/Libsβ”‚  β”‚  Bins/Libsβ”‚  β”‚  Bins/Libsβ”‚     β”‚
β”‚  β”‚  Guest OS β”‚  β”‚  Guest OS β”‚  β”‚  Guest OS β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚             Hypervisor                    β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚             Host OS + Kernel              β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Containers                          β”‚
β”‚                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚
β”‚  β”‚  App A   β”‚  β”‚  App B   β”‚  β”‚  App C   β”‚      β”‚
β”‚  β”‚  Bins/Libsβ”‚  β”‚  Bins/Libsβ”‚  β”‚  Bins/Libsβ”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚          Container Runtime                β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚          Host OS + Kernel (shared)        β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This is why containers start in milliseconds instead of minutes. No OS to boot. No hardware to emulate. Just fork a process and apply constraints.

Linux Namespaces β€” The Isolation Layer

Namespaces are the kernel feature that makes a process think it’s alone on the machine. Each namespace type isolates a different resource:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Linux Namespaces                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  PID     β”‚  Process IDs β€” container sees PID 1      β”‚
β”‚  NET     β”‚  Network stack β€” own interfaces, IPs     β”‚
β”‚  MNT     β”‚  Mount points β€” own filesystem view      β”‚
β”‚  UTS     β”‚  Hostname β€” container gets its own name   β”‚
β”‚  IPC     β”‚  Inter-process comms β€” isolated queues    β”‚
β”‚  USER    β”‚  User/group IDs β€” root inside β‰  root out  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

PID namespace is the most visible one. The main process inside a container always sees itself as PID 1. On the host, that same process has a normal PID like 48291. Run ps aux on the host and you’ll see your containerized process sitting right there alongside everything else.

NET namespace gives each container its own network stack β€” its own eth0, its own IP address, its own routing table. Docker creates a virtual bridge (docker0) and connects each container’s virtual interface to it. That’s how containers talk to each other and to the outside world.

MNT namespace controls what the filesystem looks like. The container sees the image’s filesystem as its root /, not the host’s. Bind mounts and volumes punch holes through this isolation when you need them.

USER namespace is security-critical. When user-namespace remapping or rootless mode is configured, a process that appears as root (UID 0) inside the container can map to an unprivileged high-number UID on the host. That reduces the impact of some privilege-escalation paths; it does not guarantee that every container escape lands harmlessly. Docker documents the distinction between user-namespace remapping and fully rootless mode.

None of this is Docker-specific. These are kernel primitives that have existed since Linux 3.8 (2013). Docker just made them usable.

Cgroups β€” The Resource Limits

Namespaces handle isolation. Cgroups (control groups) handle resource limits. Without cgroups, a container could consume all the host’s CPU and memory, starving everything else.

Cgroups let you set hard limits on:

  • CPU β€” how many cores or what percentage of CPU time a container gets
  • Memory β€” hard cap; exceed it and the OOM killer terminates the process
  • Disk I/O β€” throttle read/write bandwidth
  • PIDs β€” maximum number of processes (prevents fork bombs)

When you run docker run --memory=512m --cpus=1.5, Docker creates a cgroup for that container and writes those limits to the cgroup filesystem (usually under /sys/fs/cgroup/). The kernel enforces them transparently.

This is also how orchestrators like Kubernetes schedule workloads β€” they read cgroup limits to understand resource requests and allocations across nodes.

Union Filesystems and Layers β€” Why Images Are Layered

Docker images aren’t monolithic disk snapshots. They’re stacks of read-only layers, assembled using a union filesystem (commonly OverlayFS).

Every instruction in a Dockerfile creates a new layer:

FROM ubuntu:22.04          # Layer 1: base OS files
RUN apt-get install nginx  # Layer 2: nginx + dependencies
COPY app/ /var/www/html/   # Layer 3: your application code

These layers stack on top of each other:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Container Layer (read-write)   β”‚  ← runtime changes go here
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Layer 3: COPY app/             β”‚  (read-only)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Layer 2: RUN apt-get install   β”‚  (read-only)
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Layer 1: FROM ubuntu:22.04     β”‚  (read-only)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The union filesystem merges all layers into a single coherent view. When a container writes a file, it goes into a thin read-write layer on top. The image layers below are never modified β€” this is called copy-on-write.

Why does this matter?

  • Sharing: Ten containers from the same image share all the read-only layers. Only the thin writable layer is unique per container.
  • Caching: If Layer 1 and Layer 2 haven’t changed, Docker reuses them from cache during builds. Only Layer 3 gets rebuilt.
  • Distribution: When you push an image, only layers the registry doesn’t already have get uploaded.

This is why Dockerfile ordering matters. Put things that change rarely (OS, dependencies) at the top and things that change often (your code) at the bottom.

The Docker Daemon and Architecture

Docker uses a client-server architecture:

  • docker CLI β€” the command you type. It sends API requests.
  • dockerd (the daemon) β€” receives those requests, manages images, containers, networks, and volumes.
  • containerd β€” a lower-level container runtime that dockerd delegates to. It manages the container lifecycle.
  • runc β€” the actual binary that creates the container by calling the kernel’s namespace and cgroup APIs.

The chain looks like: docker CLI β†’ dockerd β†’ containerd β†’ runc β†’ your process.

This layered design exists because of the OCI (Open Container Initiative) standard. OCI defines two specs: an image format and a runtime spec. Any OCI-compliant runtime (runc, crun, gVisor, Kata) can run OCI images. This is why the same image you build with Docker runs on Podman, Kubernetes, or any other OCI-compatible platform.

What docker run Actually Does β€” Step by Step

When you type docker run -d -p 8080:80 nginx, here’s the sequence:

  1. Image pull β€” Docker checks if the nginx image exists locally. If not, it pulls layers from the registry (Docker Hub by default).

  2. Layer assembly β€” The union filesystem stacks all image layers and adds a writable container layer on top.

  3. Namespace creation β€” The kernel creates new PID, NET, MNT, UTS, IPC, and USER namespaces for the container.

  4. Cgroup setup β€” A new cgroup is created with any resource limits you specified (or defaults).

  5. Network plumbing β€” Docker creates a virtual ethernet pair (veth), attaches one end to the container’s NET namespace and the other to the docker0 bridge. It assigns an IP address and sets up iptables rules for the -p 8080:80 port mapping.

  6. Root filesystem pivot β€” The container’s MNT namespace gets the assembled layer stack as its root filesystem via pivot_root.

  7. Process execution β€” runc calls clone() with the appropriate namespace flags, applies cgroup limits, and execs the container’s entrypoint (in this case, the nginx binary).

  8. Detach β€” The -d flag means Docker returns control to your terminal while the process runs in the background.

The whole thing takes a fraction of a second. No kernel to boot, no BIOS, no hardware emulation.

Why Containers Are Fast

The performance story comes down to what containers don’t do:

  • No hypervisor overhead β€” no hardware emulation, no trap-and-emulate cycle
  • Shared kernel β€” system calls go directly to the host kernel, not through a virtualized layer
  • No OS boot β€” the container process starts immediately; there’s no init sequence
  • Minimal memory overhead β€” shared image layers mean ten containers don’t need ten copies of Ubuntu in RAM
  • Native I/O β€” disk and network operations hit real hardware through the real kernel

The tradeoff is weaker isolation compared with VMs. Containers share a kernel, so a kernel vulnerability or dangerous daemon access can cross a boundary that a VM would keep separate. Docker’s Engine security documentation recommends layered hardening rather than treating the default container as a sandbox. For multi-tenant or hostile code execution, a microVM boundary may be more appropriate.

Why AI workloads expose the abstractions

AI services put unusual pressure on every primitive described above:

  • Cgroups: a model process can consume all host RAM and trigger the OOM killer. CPU and memory limits are capacity controls, not optional polish.
  • GPU access: accelerators are exposed separately from CPU and memory. Docker’s official GPU guide requires an explicit --gpus request and compatible host drivers/toolkit.
  • Image layers: application dependencies belong in immutable layers; large model weights usually belong in a versioned cache or volume so they are not recopied with every application build.
  • Writable layer: agent artifacts, indexes, and downloads disappear with the container unless deliberately persisted.
  • Namespaces and capabilities: a coding agent that receives the host Docker socket or broad mounts can escape the intended workspace regardless of the neat process boundary.

Docker Model Runner makes these mechanics visible in a current AI workflow: models are pulled from registries, loaded only when used, exposed through compatible APIs, and run behind an isolation boundary that varies by platform and engine.

For practical configuration, use the Docker AI development guide. If the immediate symptom is model memory pressure, follow the Ollama out-of-memory guide. For untrusted agent execution, start with agent sandboxing at scale rather than assuming a default container is sufficient.

Putting It Together

Docker didn’t invent any of the underlying technology. Namespaces, cgroups, and union filesystems existed before Docker. What Docker did was package them into a workflow that developers could actually use β€” a Dockerfile, a CLI, a registry, and a daemon that wires everything together.

Understanding these internals pays off when you need to debug networking, optimize model-serving images, set resource limits, or evaluate whether to run Ollama in a container. It also makes Compose less magical: a Compose file declares which processes, resource boundaries, networks, volumes, and devices should exist together.

Containers are processes with boundaries. Once you internalize that, everything else clicks.