Lesson 01 Code to Container
Theme:

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:

NamespaceWhat it isolates
pidProcess 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.
mountFilesystem mounts. The container’s / is a separate mount namespace — it sees the image’s layered filesystem, not the host’s disk layout.
netNetwork 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.
utsHostname and domain name. hostname inside a container returns the container’s name, not the host’s.
userUID/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.
ipcSystem 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:

Critical: if you don’t set --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:

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.

Image (read-only) layer: CMD/ENV metadata layer: COPY app.py layer: pip install layer: COPY requirements.txt layer: python:3.11-slim base Container (running) writable top layer layer: CMD/ENV metadata layer: COPY app.py layer: pip install layer: COPY requirements.txt layer: python:3.11-slim base docker run (adds writable layer) An image is a stack of read-only layers. A container is the image plus one writable top layer.

5. What Docker actually does

Docker is tooling around the Linux primitives, not a runtime in itself. Its value-add breaks into four pieces:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

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.

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.

Build cache is per-machine. If you build on your laptop and push to a registry, the CI runner that later pulls the image doesn’t inherit your local cache. The pushed image layers are content-addressed and deduplicated on pull, but the build cache for CI starts cold unless you explicitly seed it (Lesson 04 covers GitHub Actions layer caching). The takeaway for now: a well-ordered Dockerfile is fast everywhere, cache or no cache, because pip re-runs only when it needs to.

References