v9.1.1
Menu

Chapter 3: Docker & Containerization

Master containerization for ML workflows — from writing Dockerfiles and multi-stage builds to GPU containers, Docker Compose, and registry distribution. Learn to package, ship, and run ML models in reproducible, portable containers.

Topics
8
Videos
3
Min read
71

Chapter Overview

Containerization has become the foundation of modern ML infrastructure. Whether you are training models on cloud GPU clusters, deploying inference endpoints, or sharing reproducible experiments with your team, containers are the universal packaging format that makes it all work.

This chapter takes you from zero to production-ready Docker skills. You will understand why containers exist (dependency hell is real), learn the core Docker workflow (images, containers, Dockerfiles), master advanced techniques (multi-stage builds, GPU passthrough, Docker Compose), and learn to distribute your images through registries.

By the end, you will be able to containerize any ML project — from a simple scikit-learn API to a multi-service inference pipeline with GPU-accelerated model serving, caching, and monitoring — and deploy it identically on your laptop, your team's servers, or a cloud Kubernetes cluster.

Chapter Roadmap

Click any topic to jump in

Explainer video
1
Why Containers?

Dependency hell, reproducibility, ML environment drift — the problems containers were built to solve.

Dependency HellContainersContainers vs VMsOCI StandardML ReproducibilityDocker DesktopPodman
Why leads to how
2
Docker Basics

Images, containers, run/ps/exec, lifecycle — the everyday commands you'll type a hundred times a week.

ImagesContainersdocker rundocker psdocker execContainer LifecycleLazydocker
3
Writing Dockerfiles

FROM, RUN, COPY, CMD vs ENTRYPOINT, layer caching — how reproducible images are actually authored.

FROMRUNCOPYWORKDIRCMD vs ENTRYPOINTLayer Caching.dockerignoreHadolintDive
Basics → authoring images

From running images to writing them

4
Multi-Stage Builds

Builder vs runtime stages, COPY --from, BuildKit — shrinking 8 GB ML images down to something deployable.

Multi-Stage BuildsBuilder StageRuntime StageCOPY --from=builderSize ReductionBuildKitModel Optimization
5
Volumes & Networking

Bind mounts, named volumes, port mapping, bridge networks — how containers persist data and talk to each other.

VolumesBind MountsNamed Volumes vs Bind MountsPort MappingDocker NetworksBridge NetworkVolume Drivers
Dockerfiles → optimized builds
6
Docker Compose

docker-compose.yml services, depends_on, shared networks — the multi-container dev environment in one file.

docker-compose.ymlServicesdepends_onShared NetworksEnvironment Variablesdocker compose up/downPortainer
7
GPU in Docker

NVIDIA Container Toolkit, --gpus, CUDA base images, version matching — the missing pieces for GPU training inside containers.

NVIDIA Container Toolkit--gpus FlagCUDA Base Imagesnvidia-smi in ContainerCUDA Version MatchingMulti-GPU Trainingnvidia-docker (Legacy)
Containers need storage and a network
8
Registry & Distribution

Docker Hub, ECR, image tags, semantic versioning, push/pull — how images get shared across machines and clusters.

Container RegistryDocker HubAmazon ECRImage TagsSemantic Versioningdocker push/pullSkopeocrane

Topics

8 topics in this chapter

A model trains perfectly on your laptop. A colleague clones the repository, runs the same script, and gets a crash, or worse, a quietly different accuracy. The Python version differs, a system library is missing, or the installed PyTorch build does not support their GPU. Multiply that by a training job, an inference API and a data pipeline, each needing its own stack, and moving work between laptop, CI and production becomes the slowest part of the project.

This chapter is about the standard fix: package the code together with its entire user-space environment, so the thing that runs is the thing you tested. A container does exactly that. It is an ordinary Linux process tree, isolated with kernel namespaces and limited with cgroups, running from an image that holds the interpreter, libraries and files. Because containers share the host kernel, they start in well under a second and weigh megabytes rather than gigabytes.

This first topic covers why that matters for ML, how containers differ from virtual machines, the OCI standard that lets every tool share one image format, and the two common engines, Docker Desktop and Podman.

Definition

A container is a process, or group of processes, that runs from an image containing its own filesystem and dependencies, and is isolated from the rest of the host by Linux namespaces (process IDs, mounts, network, users) and limited by cgroups. All containers on a host share that host's kernel, unlike virtual machines, which each boot their own.

In this topic

  1. 1Dependency Hell
  2. 2Containers
  3. 3Containers vs VMs
  4. 4OCI Standard
  5. 5ML Reproducibility
  6. 6Docker Desktop
  7. 7Podman
1 of 7
Dependency Hell

Conflicting Python versions, system libraries, and CUDA drivers across environments break ML reproducibility. A model that trains on Python 3.11 with CUDA 12.1 may silently produce wrong results or crash outright on Python 3.9 with CUDA 11.6.

The problem is compounded by transitive dependencies — your project depends on library A, which depends on library B version 2.x, but another dependency requires library B version 3.x. Without isolation, these conflicts are nearly impossible to resolve across a team.

Mathematical Intuition

Conflicts grow with the number of things forced to share one environment. If kk projects each pin versions of the same libraries, a shared environment works only if every pin agrees, and each new project adds more constraints that can clash. Giving every project its own environment removes the cross-project constraints entirely: each image only has to satisfy its own pins. The remaining shared dependency is the host kernel and, for GPUs, the driver, which is why those two are the only things containers cannot make portable.

Example:

Your training script runs on your machine but crashes on a colleague's with: RuntimeError: CUDA error: no kernel image is available. Both machines have NVIDIA GPUs. What is the likely cause?

2 of 7
Containers

Lightweight, portable units that package code, dependencies, and runtime into a single, reproducible image. A container includes everything the application needs to run — Python interpreter, pip packages, system libraries, config files — isolated from the host system.

Containers use OS-level virtualization (namespaces and cgroups) to provide process isolation without the overhead of a full virtual machine. They share the host kernel, which makes them start in seconds and use minimal extra memory.

Mathematical Intuition

A container adds almost no work at run time, because a containerized process makes the same system calls to the same kernel as any other process. Felter and colleagues found containers matched or beat KVM virtual machines in nearly every benchmark, with overhead appearing mainly in Docker's NAT networking. Start-up costs are setup work: namespaces, cgroups and the overlay mount. Measured on one host, docker run of a trivial Alpine container took about 0.55 seconds end to end through the CLI and daemon, and most of that is client and daemon overhead rather than the kernel.

3 of 7
Containers vs VMs

VMs virtualize hardware — each VM runs a full guest operating system (1-10 GB), boots in 30-60 seconds, and is managed by a hypervisor. Containers virtualize the OS — they share the host kernel, weigh megabytes, and start in under a second.

For ML workloads, this difference is transformative. You can run dozens of container experiments on a single GPU machine, each with different dependency sets, without the overhead of booting separate operating systems. VMs still have their place (strong security isolation, different OS kernels), but containers dominate ML workflows.

Mathematical Intuition

The difference comes from what each one boots. A VM boots a guest kernel and init system and reserves memory for them, so classic VMs take tens of seconds and hundreds of megabytes before your code runs. A container starts a process on the already running kernel. The gap is narrowing at the VM end: Firecracker microVMs boot in about 125 ms with under 5 MiB of overhead, which is why AWS Lambda uses them. They keep a VM's stronger isolation boundary while approaching container start-up costs, so the choice is increasingly about isolation rather than speed.

Example:

You need to test your ML inference API against 5 different Python versions (3.8-3.12). Compare the VM approach vs. the container approach in terms of disk space and startup time.

4 of 7
OCI Standard

The Open Container Initiative, founded in 2015 under the Linux Foundation, publishes three specifications: the image spec (how an image is laid out as a manifest, a config and content-addressed layer archives), the runtime spec (how a runtime such as runc starts a container from an unpacked bundle) and the distribution spec (the HTTP API registries use). Because Docker, Podman, Buildah, containerd and CRI-O all follow them, an image built with one tool runs under the others and can be stored in any compliant registry. That is why you can build with Docker on a laptop and run the same image on a Kubernetes cluster whose runtime is containerd.

Mathematical Intuition

The OCI image spec defines images as a JSON manifest + content-addressed layer tarballs hashed with SHA-256. Two images that share base layers also share storage — the deduplication factor on a host running NN containers from one base is ≈N\approx N. This is why pulling 100 nginx variants on the same host costs ~one nginx worth of disk.

5 of 7
ML Reproducibility

An ML result depends on the code, the data and the environment. An image captures the environment: the Python version, every installed package, system libraries, CUDA and cuDNN. Pin each one, refer to the base image by digest, and record the image digest with every training run, so the exact environment can be pulled again years later. An image does not make results bit-identical on its own. GPU kernels can be nondeterministic, and the host driver and GPU model still come from outside the image. Fixed seeds, deterministic algorithm settings and logged hardware close most of the remaining gap.

Mathematical Intuition

An image digest is a SHA-256 hash of the image manifest, which lists the hashes of the config and every layer. Changing any byte of any file changes a layer hash, which changes the manifest and therefore the digest. A 64-hex-character string identifies hundreds of pinned components at once, and the chance of two different images colliding is about 2−1282^{-128} even across billions of images. That is why logging the digest, rather than a tag, is enough to name the environment of a run exactly.

6 of 7
Docker Desktop

Docker Desktop packages the Docker engine, CLI, BuildKit, Compose and an optional single-node Kubernetes for macOS, Windows and Linux desktops. Containers need a Linux kernel, so on macOS and Windows Desktop runs a lightweight Linux VM (Apple's virtualization framework or WSL 2) and forwards the CLI, ports and file sharing into it. Settings control how much CPU, memory and disk that VM gets, which caps what your containers can use. Licensing matters for companies: it is free for personal use, education, open source and businesses with fewer than 250 employees and under 10 million dollars in revenue, and needs a paid subscription otherwise.

Mathematical Intuition

On macOS and Windows every bind-mounted file access crosses from the Linux VM to the host filesystem and back. For a few large reads that costs little, but workloads touching thousands of small files, such as importing a big Python package tree or a node_modules directory from a bind mount, pay the crossing per file and can be several times slower than on native Linux. VirtioFS and Docker's synchronized file shares reduce the gap. The practical fix is to keep dependencies inside the image or a named volume and bind-mount only your source.

7 of 7
Podman

Podman is a container engine from Red Hat with a Docker-compatible CLI, so most docker commands work unchanged and alias docker=podman is common. Two design choices set it apart. It has no long-running daemon: each container is a child of the podman process or a systemd unit. And it runs rootless by default, using user namespaces so that root inside the container maps to your unprivileged user on the host. A container escape then lands as an ordinary user, not as root. Podman also groups containers into pods, like Kubernetes, and can generate Kubernetes YAML or systemd units from running containers.

Mathematical Intuition

Rootless containers rely on user-namespace ID mapping. Your user is granted a range of subordinate IDs, typically 65,536 of them in /etc/subuid, and container UID 0 maps to your own UID while container UIDs 1 to 65,535 map into that range. A process that escapes therefore holds only the rights of an unprivileged host user. The cost is that some things need extra work: binding ports below 1024 and some network setups require privileges, and files written to bind mounts show up owned by the mapped IDs rather than by root.

Theory Exercise

Problem:

An ML team has three projects: a PyTorch 2.1 training pipeline, a TensorFlow 2.15 inference server, and a scikit-learn data preprocessing job. All three need to run on the same GPU machine. Explain why containers are a better solution than virtual environments (venv/conda) for this scenario.

Hints:
  • Think about system-level dependencies beyond Python packages — CUDA, cuDNN, system libraries
  • Consider what happens when two projects need different versions of the same system library (e.g., libcudnn)
  • Think about deployment: how would you move each project to a cloud GPU instance?