Docker from First PrinciplesPost 10 of 18 · 18 of 18 written
- How to use this guide
- 01The problem Docker solves
- 02What a container really is
- 03Images: layers, hashes and registries
- 04Docker's architecture
- 05Container lifecycle
- 06Dockerfiles in depth
- 07Storage: where data lives
- 08Networking
- 09Docker Compose for multi-service AI apps
- 10GPUs in containers
- 11Building good images for AI workloads
- 12Serving and production patterns
- 13Security
- 14Debugging and introspection
- 15Beyond Docker
- 16Hands-on learning path
- 17Question bank
- 18Glossary and cheat sheet
10. GPUs in containers
To a container, an NVIDIA GPU is a set of device files (/dev/nvidia0, /dev/nvidiactl, /dev/nvidia-uvm) plus driver libraries that must exactly match the host’s kernel driver. The NVIDIA Container Toolkit injects both when the container starts. So the image carries CUDA libraries but never the driver, and one image runs on any host whose driver is new enough.
The four pieces and who owns them
| Piece | Examples | Lives in | Rule |
|---|---|---|---|
| Hardware | A100, H100, L4, RTX 4090 | The machine | Newer GPU generations need newer CUDA builds |
| Kernel driver | nvidia.ko, driver branch 550, 570… |
Host only | Must be installed on the host |
| Driver user-space libraries | libcuda.so, libnvidia-ml.so, nvidia-smi |
Host, injected into the container | Must match the kernel driver exactly, so they can’t be in the image |
| CUDA runtime and math libraries | libcudart, cuBLAS, cuDNN, NCCL |
The image, often inside the PyTorch wheel | Need a driver at least as new as their CUDA version |
The NVIDIA Container Toolkit
Install the NVIDIA driver and the Container Toolkit on the host, then register it with Docker: sudo nvidia-ctk runtime configure --runtime=docker and restart the daemon. From then on, --gpus all tells Docker to have the toolkit mount the GPU devices, the matching driver libraries and nvidia-smi into the container.
The smoke test that proves the whole chain works:
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smiChoosing GPUs: --gpus all, --gpus 2 (any two), or --gpus '"device=0,2"' (specific ones; the nested quotes are needed). Inside the container, the selected GPUs are renumbered from 0. Newer setups can also use the Container Device Interface (CDI), a vendor-neutral spec file describing devices, with --device nvidia.com/gpu=all.
CUDA compatibility, decoded
- The “CUDA Version” in
nvidia-smiis a ceiling, not what’s installed: it’s the newest CUDA runtime that this driver supports. A container with CUDA 12.1 on a driver showing 12.4 is perfectly fine. - Newer driver, older CUDA: works. Older driver, newer CUDA: fails with “CUDA driver version is insufficient for CUDA runtime version”. Within one major CUDA version, NVIDIA’s minor-version compatibility relaxes this somewhat; check NVIDIA’s CUDA compatibility page for exact driver minimums.
- PyTorch wheels bring their own CUDA runtime (the
cu121,cu124,cu128builds). So a plainpython:3.12-slimbase plus a CUDA PyTorch wheel runs on GPU; you don’t need annvidia/cudabase. You need a CUDAdevelimage only to compile CUDA code: custom kernels, or extensions like flash-attn built from source. - New GPU, old build: “no kernel image is available for execution on the device” means the binary wasn’t compiled for your GPU’s architecture. Very new GPUs (for example the Blackwell generation) need recent CUDA and PyTorch builds.
Base image flavours
| Image | Contains | Typical size | Use for |
|---|---|---|---|
nvidia/cuda:<ver>-base-<os> |
CUDA runtime core only | Small | Running prebuilt binaries, smoke tests |
nvidia/cuda:<ver>-runtime-<os> |
Plus math libraries and NCCL | Medium | Running CUDA apps that don’t bundle libraries |
nvidia/cuda:<ver>-cudnn-runtime-<os> |
Plus cuDNN | Medium to large | Frameworks expecting system cuDNN |
nvidia/cuda:<ver>-devel-<os> |
Plus compiler (nvcc) and headers |
Large | Builder stage for compiling extensions |
pytorch/pytorch:<tag>-runtime / -devel |
Conda-based PyTorch with CUDA | Large | Quick start with a known-good stack |
nvcr.io/nvidia/pytorch:<release>-py3 (NGC) |
NVIDIA’s tuned PyTorch, CUDA, cuDNN, NCCL and tools | 10 GB and up | Serious training on NVIDIA hardware, monthly releases |
Shared memory
Docker gives each container only 64 MB of /dev/shm. PyTorch’s DataLoader with num_workers > 0 moves batches between processes through shared memory, so it fails with a “bus error” or “DataLoader worker killed” in Docker even though the same code works on bare metal. NCCL also uses shared memory for GPU-to-GPU traffic within a node. Fix it with --shm-size=16g or --ipc=host; model servers such as vLLM document one of these as required.
Multi-GPU and multi-node
On one machine, --gpus all plus torchrun --nproc_per_node=<gpus> inside the container works like bare metal, provided shared memory is large enough. Across machines, add host networking and InfiniBand access (Part 8), and usually let a scheduler (Slurm with Pyxis and Enroot, or Kubernetes with the NVIDIA GPU Operator) handle placement.
Docker cannot limit GPU memory: --memory caps system RAM only. Two containers on the same GPU can push each other out of memory. Real isolation comes from Multi-Instance GPU (MIG) on A100 and H100 class cards, which splits a GPU into hardware-isolated slices that appear as separate devices, or from framework settings such as vLLM’s --gpu-memory-utilization.
AMD GPUs
AMD’s ROCm stack works similarly but without a toolkit layer: pass the devices directly with --device /dev/kfd --device /dev/dri and use ROCm images such as rocm/pytorch.
On your Mac
Containers in Docker Desktop run in a Linux VM with no access to the Apple GPU, so CUDA is unavailable and PyTorch’s Metal backend (MPS) doesn’t work inside containers; everything runs on CPU. The working pattern: run GPU-hungry local inference natively (Ollama, llama.cpp, MLX use Metal) and call it from containers through host.docker.internal. Docker Desktop’s Model Runner feature takes the same approach, running models on the host and exposing an API to containers.
For CUDA work, rent a Linux GPU machine and drive it from your Mac’s terminal with a Docker context over SSH:
docker context create gpu-box --docker "host=ssh://ulvi@<server-address>"
docker context use gpu-box
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
docker context use default # back to the Mac's engineEvery docker command now runs on the remote engine, including builds, which then happen natively on amd64.