Code to Container
Lesson 01 · Cloud Infrastructure & DevOps · ~12 min read
1. Why “works on my machine” still exists in 2026
You push a Python script that has worked flawlessly on your laptop for three weeks. The CI runner pulls the branch and immediately fails: libssl version mismatch. Staging deploys successfully but then crashes under load because the production VM is running Python 3.12 while you developed against 3.11 and one dependency changed its behavior between the two versions. A colleague clones the repo on a fresh MacBook, runs python app.py, and gets a different error entirely because they don’t have the system packages you silently depend on.
None of these problems are bugs in your code. They are environment divergence — the gap between the context your code was written in and the context it is expected to run in. They are not new. Sysadmins have fought them since the 1970s. What is new is a practical solution that ships in the standard developer toolchain.
Containers don’t make environment divergence impossible. They make the entire environment ship with the code, so every machine that runs your container runs the exact same filesystem your tests ran against. The lesson’s core claim: a container is a process running inside Linux namespaces and cgroups, packaged with its dependencies in an immutable image. Everything else — Docker, registries, Kubernetes — is tooling built on top of that.
2. Linux primitives: namespaces
A container is not a VM. It does not run its own kernel. It is a process (or a small tree of processes) on the host, isolated from other processes by a set of kernel features called namespaces. Each namespace type wraps a different operating-system resource:
| Namespace | What it isolates |
|---|---|
pid | Process IDs. The container sees its main process as PID 1. Signals sent to PID 1 in the container do not cross into the host’s pid namespace. |
mount | Filesystem mounts. The container’s / is a separate mount namespace — it sees the image’s layered filesystem, not the host’s disk layout. |
net | Network interfaces, routing tables, iptables rules, loopback. The container gets its own eth0. docker run -p 8000:8000 is an explicit forwarding rule between the host’s net namespace and the container’s. |
uts | Hostname and domain name. hostname inside a container returns the container’s name, not the host’s. |
user | UID/GID mapping. When user namespaces are enabled, root (UID 0) inside the container maps to an unprivileged UID on the host. This is one of the key mechanisms that prevents container escapes. |
ipc | System V semaphores, message queues, and shared memory segments. Processes in different containers cannot communicate via these mechanisms accidentally. |
When you run docker run, the Docker daemon (or containerd, or runc) calls the clone(2) syscall with flags that create these namespaces, then execs your process inside the new namespace set. There is no hypervisor, no separate kernel boot, no emulated hardware. The overhead versus a bare fork() is measured in milliseconds.
Namespaces have existed in the Linux kernel since the mid-2000s (the pid and mount namespaces landed in 2.6.24, released in 2008). Docker didn’t invent them; it built a usable developer experience on top of them.
3. Linux primitives: cgroups
Namespaces isolate what a process can see. Control groups (cgroups) limit how much of each resource it can consume. Without cgroups, a single container could eat all host RAM and starve every other process on the machine. With cgroups, Docker can enforce per-container resource budgets:
- CPU —
docker run --cpus 0.5limits the container to 50% of one core. This maps to cgroup files under/sys/fs/cgroup/cpu/. - Memory —
docker run --memory 256mtriggers the Linux OOM killer for that container when it exceeds 256 MB. The rest of the host is protected. - Block IO —
--blkio-weightand device-specific IO caps prevent a noisy container from saturating the disk. - PIDs —
--pids-limitcaps the number of processes the container can create, blocking fork-bomb attacks.
--memory, Docker doesn’t set one for you. A container with a memory leak will eat host RAM until the kernel OOM-kills random processes — possibly including unrelated services. Always set memory limits in production.
cgroups also enable the high container density that makes them economically interesting. A single cloud VM with 8 cores and 32 GB RAM can comfortably host hundreds of small containers, each believing it has exclusive access to its allotted resources. The same VM might host a dozen VMs.
4. Images, containers, and layers
Three terms that Docker overloads but which have precise definitions:
- Image — an immutable, content-addressed bundle of filesystem layers plus a JSON metadata manifest (default command, env vars, entrypoint, exposed ports). Each layer is a tarball; layers stack using overlay-fs so only the diffs are stored. An image never changes after it is built.
- Container — a running (or stopped) process tree backed by an image’s read-only layers, with a thin writable layer on top. The writable layer is discarded when you
docker rmthe container unless you mount a volume. Many containers can run from one image simultaneously. - Layer — one filesystem diff. Each Dockerfile instruction that changes the filesystem (FROM, RUN, COPY, ADD) produces one layer. Layers are content-addressed: two identical layers share one copy on disk across every image that uses them.
The content-addressing model is what makes container density practical. A 1.2 GB python:3.11 base image isn’t 1.2 GB per container. It is 1.2 GB shared across every container built from it, with only the writable top layer per running container consuming additional disk. This is why a single host can run hundreds of containers while a VM requires a full OS copy per instance.
5. What Docker actually does
Docker is tooling around the Linux primitives, not a runtime in itself. Its value-add breaks into four pieces:
- Image format — the OCI (Open Container Initiative) image spec: layered tarballs plus a JSON manifest. The format is runtime-agnostic. Docker, containerd, podman, and CRI-O can all push, pull, and run OCI images.
- Registry protocol — a standardized HTTP API for pushing and pulling images. Docker Hub, GitHub Container Registry (GHCR), Amazon ECR, and Google Artifact Registry all speak the same protocol.
- Build language — the
Dockerfile: a deterministic, line-by-line recipe for assembling a layered image from a base + instructions. Reproducible given the same base digest and the same inputs. - Runtime CLI —
docker run,docker exec,docker logs,docker ps, and the daemon that orchestrates the underlying calls to runc / containerd.
In production you will encounter other runtimes built on the same primitives: containerd (used by most managed Kubernetes clusters), CRI-O (used by OpenShift), podman (rootless alternative on Linux). The Dockerfile and OCI image format are portable across all of them. Learning Docker first gives you the concepts that transfer everywhere.
6. The minimal Dockerfile, line by line
The Dockerfile used in Lesson 01’s lab is deliberately the simplest possible version that actually works:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Line by line:
FROM python:3.11-slim— pulls the base image from Docker Hub.slimis a Debian-based variant stripped of non-essential packages (~120 MB vs ~1.2 GB for the full image). Every subsequent instruction builds on top of this layer.WORKDIR /app— sets the working directory for all subsequent instructions and for the running container. Creates the directory if it doesn’t exist. Prefer over bareRUN mkdir.COPY requirements.txt .— copies one file from the build context (the directory you passed todocker build) into/app/requirements.txt. One file at a time, placed early, because it changes rarely — this is a build-cache optimization (see Section 7).RUN pip install --no-cache-dir -r requirements.txt— runs inside a throwaway intermediate container; the result is committed as a layer.--no-cache-dirtells pip not to write a download cache into the layer, keeping it smaller.COPY app.py .— separate COPY instruction for the application code. Placed after the pip install so that code edits don’t bust the cached pip layer.EXPOSE 8000— documentation only. It tells humans (and tooling likedocker-compose) that the container expects traffic on port 8000. It does not open the port;-p 8000:8000ondocker rundoes that.CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]— the default command executed when the container starts. The exec form (JSON array) runs uvicorn directly as PID 1 without a shell wrapper, so it receives SIGTERM and can shut down cleanly. If you used the shell form (CMD uvicorn ...), a/bin/shwould be PID 1 and uvicorn would not receive SIGTERM.
7. Build-time caching and why it matters
Docker caches each layer keyed by: the instruction text + all parent layer digests + any referenced file content. If none of those change, Docker skips running the instruction and reuses the cached layer. What takes 60 seconds on a cold build takes 0.1 seconds on a warm one.
The ordering rule that follows from this: place instructions in order from least-frequently-changing to most-frequently-changing.
FROM— changes only when you upgrade the base image (rare)COPY requirements.txt+RUN pip install— changes when you add or remove a dependency (occasional)COPY app.py— changes on every code edit (frequent)
Reversing this order — COPY app.py first, pip install second — means every code edit re-downloads every Python wheel. On a project with 30 dependencies, that’s potentially several minutes per iteration instead of seconds.
References
- Docker docs — Build images with BuildKit (
docs.docker.com/build/buildkit/) - OCI image-spec (
github.com/opencontainers/image-spec) - Linux containers in 500 lines of code — Lizzie Dixon’s classic walkthrough of namespaces + cgroups from first principles
- This course’s sibling: Linux Networking Lesson 02 — network namespaces deep dive for the lower-layer fundamentals