Your model runs fine on your laptop’s GPU, then dies the moment you containerize it. CUDA not found, driver mismatch, a 9GB image that takes twenty minutes to push — I have hit every one of these, and they all come from the same root cause: the host, the runtime, and the image were never set up as one system.
In this deep-dive I show you the complete 2026 setup: install the NVIDIA Container Toolkit, reserve GPUs in Compose with the verified deploy syntax, pick the right CUDA base image, and shrink your Python AI app with a multi-stage build.
1. The Problem: It Works on My GPU (and Nowhere Else)
Shipping a GPU-powered AI app has three moving parts that must agree with each other: the NVIDIA driver on the host, the container runtime that exposes the GPU, and the CUDA libraries inside the image. When any one of them drifts — a tutorial pins an old nvidia-docker2 flag, the Compose file uses a 2021-era runtime key, the Dockerfile ships a devel image to production — you get cryptic errors at 2am and images measured in gigabytes.
The Most Common Mistake
Installing the full CUDA Toolkit on the host. You do not need it there. The container carries its own CUDA userspace libraries; the host only needs the NVIDIA driver plus the NVIDIA Container Toolkit, which exposes the driver into containers at runtime.
The goal of this guide is a reproducible stack you can commit to git: one verified host setup, one compose.yaml with explicit GPU reservations, and one multi-stage Dockerfile that builds in devel and ships in runtime. Every command below was verified against the official NVIDIA and Docker docs.
2. Minimal Concepts You Actually Need
Only four ideas matter. Understand these and every GPU error message suddenly makes sense.
Driver vs Toolkit vs Runtime
Host setup · NVIDIA docs
The host keeps the NVIDIA driver — nothing else. The NVIDIA Container Toolkit (nvidia-ctk) configures your container runtime so the driver is injected into containers on demand. No CUDA install on the host, ever.
CUDA Flavors: base, runtime, devel
nvidia/cuda · Docker Hub / NGC
Tags follow {version}-{flavor}-{os}, e.g. 12.8.0-runtime-ubuntu22.04. base (~300MB) runs prebuilt binaries, runtime (~2GB) runs frameworks, devel (~6GB) compiles. Ship runtime, compile in devel.
Compose Device Reservations
deploy.resources · Docker docs
GPUs are reserved per service under deploy.resources.reservations.devices with driver: nvidia, capabilities: [gpu], and either count or device_ids — never both. capabilities is mandatory.
nvidia-smi Is the Source of Truth
Verify everything with it
Run nvidia-smi on the host first. If the host cannot see the GPU, no container ever will. Then run it inside the container to prove the whole chain works end to end.
The Golden Rule of CUDA Images
You almost certainly want runtime, not devel. If you are pip-installing PyTorch and running a script, nvcc is never called — shipping devel to production means shipping a compiler nobody uses. Compile in devel, ship in runtime.
3. Tutorial: GPU AI App with Compose, Step by Step
We will go from a bare Linux host with an NVIDIA GPU to a running Python inference API with GPU access, in six steps. Tested flow, official commands only.
Step 1
Confirm the host sees the GPU
Before touching Docker, prove the driver works. This must list your GPUs — if it fails, fix the driver first; nothing downstream can help.
nvidia-smi
Step 2
Install the NVIDIA Container Toolkit
These are the current repository steps from the official install guide (Ubuntu/Debian shown; RHEL-family uses the equivalent dnf repo). The toolkit package plus nvidia-ctk runtime configuration is all a Docker host needs.
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart dockerStep 3
Smoke-test with an official CUDA image
Run nvidia-smi inside a throwaway container. This proves driver → toolkit → runtime → image in one line. Pin an explicit tag — never a floating one.
docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu22.04 nvidia-smi
Step 4
Reserve the GPU in compose.yaml
This is the verified Compose syntax from the Docker GPU docs: one device entry with driver nvidia, capabilities [gpu], and count 1. Use count: all for every GPU, or device_ids like [“0”] to pin specific ones — but never combine count and device_ids in the same entry.
services:
api:
build: .
ports:
- "8000:8000"
volumes:
- model-cache:/models
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
model-cache:Step 5
Multi-stage Dockerfile: compile big, ship small
Build on runtime for pure-Python apps, or compile native extensions in devel and copy only the artifacts into runtime. This pattern routinely turns a 6GB image into a few hundred megabytes.
# Stage 1: build native extensions (only if you compile anything)
FROM nvidia/cuda:12.8.0-devel-ubuntu22.04 AS builder
WORKDIR /build
COPY requirements.txt .
RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-pip \
&& rm -rf /var/lib/apt/lists/* \
&& pip3 install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: ship lean runtime
FROM nvidia/cuda:12.8.0-runtime-ubuntu22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /install /usr/local
COPY . /app
WORKDIR /app
CMD ["python3", "main.py"]Step 6
Up, then prove the GPU is visible
Build, start, and check from inside the running service. The torch one-liner is the real acceptance test for a PyTorch app.
docker compose up --build -d docker compose exec api nvidia-smi docker compose exec api python3 -c "import torch; print(torch.cuda.is_available())"
✅ Acceptance checklist
nvidia-smi works on the host, in docker run, and in docker compose exec — and torch.cuda.is_available() returns True. If all three pass, your stack is reproducible on any identical host with one compose up.
4. Common Errors (and the Exact Fix)
These five failures cover roughly 90% of Docker GPU pain. Each has one deterministic fix — no reinstall-the-world needed.
“could not select device driver nvidia”
The toolkit is missing or Docker was not restarted after configuring it. Fix: install nvidia-container-toolkit, run sudo nvidia-ctk runtime configure --runtime=docker, then sudo systemctl restart docker.
Compose errors on the devices block
You either omitted capabilities: [gpu] (mandatory — deployment fails without it) or combined count with device_ids (mutually exclusive). Keep exactly one of them per device entry.
CUDA version vs driver mismatch
Drivers are backward compatible, not forward: an old host driver cannot run a newer CUDA container. Fix: upgrade the host driver, or pin the image to a CUDA version your driver supports.
Production image is 6–8GB
You shipped a devel or cudnn-devel tag. Fix: multi-stage build — compile in devel, copy artifacts into base or runtime. For pure pip apps, just start FROM runtime directly.
“manifest not found” on a tag that used to work
Docker Hub retires old CUDA tags over time. Fix: pin explicit tags and prefer nvcr.io (NVIDIA’s registry) for long-lived reproducible builds.
Golden rule
Debug in layers, always in this order: host nvidia-smi → docker run --gpus all nvidia-smi → compose exec nvidia-smi → app-level check. The first layer that fails tells you exactly which component to fix.
Conclusion
GPU containers feel fragile only when the three layers are improvised separately. With the system from this guide — driver plus toolkit on the host, explicit device reservations in Compose, pinned CUDA flavors, multi-stage images — your AI app becomes boring infrastructure: one repo, one compose up, same GPU behavior everywhere.
Start from the acceptance checklist above on your own machine, then pin your tags and commit. If you are deploying open models on that stack next, my GPT-OSS and Qwen3 guides show exactly what to run on top of it.
The Stack, in One Glance
Host
- • NVIDIA driver + nvidia-smi
- • nvidia-container-toolkit
- • nvidia-ctk + docker restart
Compose
- • deploy.resources.reservations
- • driver nvidia + [gpu]
- • count XOR device_ids
Image
- • Pinned nvidia/cuda tag
- • devel builds, runtime ships
- • nvcr.io for long-lived pins



