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.
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.
Click any topic to jump in
Dependency hell, reproducibility, ML environment drift — the problems containers were built to solve.
Images, containers, run/ps/exec, lifecycle — the everyday commands you'll type a hundred times a week.
FROM, RUN, COPY, CMD vs ENTRYPOINT, layer caching — how reproducible images are actually authored.
From running images to writing them
Builder vs runtime stages, COPY --from, BuildKit — shrinking 8 GB ML images down to something deployable.
Bind mounts, named volumes, port mapping, bridge networks — how containers persist data and talk to each other.
docker-compose.yml services, depends_on, shared networks — the multi-container dev environment in one file.
NVIDIA Container Toolkit, --gpus, CUDA base images, version matching — the missing pieces for GPU training inside containers.
Docker Hub, ECR, image tags, semantic versioning, push/pull — how images get shared across machines and clusters.
Every ML engineer has experienced this: your model trains perfectly on your laptop, but when your colleague clones the repo, nothing works. Different Python version, missing system libraries, conflicting CUDA drivers. This is dependency hell, and it's the single biggest source of wasted time in ML projects.
The problem compounds in production. Your training script needs Python 3.11, PyTorch 2.1, CUDA 12.1, and a specific version of OpenCV compiled with FFmpeg support. Your inference API needs a different set of dependencies. Your data pipeline uses yet another environment. Managing all of this across development, staging, and production is a nightmare.
Containers solve this by packaging your code, dependencies, and runtime environment into a single, portable unit. A container runs identically on your laptop, your colleague's machine, a CI/CD server, and a production Kubernetes cluster. The environment is defined in code (a Dockerfile), versioned alongside your application, and reproducible down to the exact byte.
Unlike virtual machines, containers share the host OS kernel, making them lightweight (megabytes, not gigabytes) and fast to start (seconds, not minutes). This efficiency makes containers the default deployment unit for modern ML systems — from training jobs to model serving endpoints.
Tools like Docker Desktop provide a full container runtime with a GUI for managing images and containers. Podman is a daemonless alternative that runs containers without root privileges, making it popular in security-conscious environments.
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.
Without containers, a host with services has possible dependency version combinations, where is the number of versions per dep. For 5 services with 10 deps each, that's — astronomical. Containers cut this to independent environments with interactions, eliminating combinatorial explosion entirely.
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?
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.
A container is a process tree running inside a private namespace (PID, mount, network, UTS, IPC, user) and constrained by cgroups. The kernel is shared with the host — there's exactly one OS, ms cold-start overhead beyond a normal fork+exec, and memory cost is just the process working set. That's why containers replaced 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.
VMs run a full guest kernel; containers share the host kernel. A VM costs MB of RAM minimum (kernel + init) and s to boot. A container costs MB of RAM and ms to start. For ephemeral workloads, this memory and startup-time difference is the entire reason serverless and CI exist.
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.
The Open Container Initiative defines image and runtime specs — Docker, Podman, and Kubernetes all speak the same language. An OCI-compliant image built with Docker can run on Podman, CRI-O, or any OCI-compatible runtime without modification.
This standardization means you are not locked into Docker. You can build with Docker, run with Podman, and deploy on Kubernetes — all using the same image format.
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 containers from one base is . This is why pulling 100 nginx variants on the same host costs ~one nginx worth of disk.
Pin every dependency (Python version, pip packages, system libraries, CUDA version) in a Dockerfile for bit-for-bit reproducibility. When you share a Dockerfile and a requirements.txt with locked versions, anyone can reproduce your exact environment — months or years later.
This is not just convenience; it is a scientific requirement. ML experiments must be reproducible, and the environment is as much a part of the experiment as the code and data.
An ML training run depends on Python version, pip packages, CUDA version, cuDNN version, NCCL, OS libs — moving parts. Without containers, reproducing a year-old result requires correct version pins. With a Docker image hash, the entire environment collapses to a single -byte SHA-256 — the cheapest reproducibility hash you'll ever ship.
Full container runtime with GUI, built-in Kubernetes, and Docker Compose support for local development. Docker Desktop provides a system tray application that manages the Docker daemon, shows running containers, and lets you configure resource limits (CPU, memory, disk) allocated to containers.
On macOS and Windows, Docker Desktop runs a Linux VM (HyperKit / WSL2) and tunnels Docker calls over a Unix socket. Per-call overhead is ms on top of native Docker. Filesystem performance for bind mounts crosses a hypervisor boundary — typically – slower than native — which is why bind-mounted node_modules is the canonical Mac dev pain point.
Daemonless, rootless container engine — a drop-in Docker replacement without requiring a background daemon or root access. Podman uses the same CLI commands as Docker (you can alias docker=podman), making migration trivial.
The key advantage is security: Podman runs containers as your regular user, not as root. This eliminates an entire class of container escape vulnerabilities.
Podman is a daemonless, rootless drop-in for Docker. Each container runs as a child of the user's shell (or systemd user instance), eliminating the privileged dockerd attack surface. The trade-off: no docker socket means no docker.sock mount escape vector — but also no centralized API for monitoring tools that expect it.
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.