GPU and Device Access
Containers see no host hardware by default beyond a minimal set of pseudo-devices. Anything physical — a GPU for model inference, an Intel iGPU for video transcoding, a USB radio for home automation — has to be passed in explicitly.
For NVIDIA GPUs that means installing the NVIDIA Container Toolkit and using --gpus.
Everything else is generally a --device mapping plus getting the group permissions right.
Table of Contents
- NVIDIA GPUs
- Installing the Container Toolkit
- Running GPU Containers
- GPUs in Compose
- Integrated and AMD Graphics
- Other Host Devices
- Rootless and Swarm
- Troubleshooting
NVIDIA GPUs
How It Works
The host provides the NVIDIA kernel driver. The container provides the CUDA userspace libraries. The NVIDIA Container Toolkit bridges the two: it hooks container creation, injects the correct driver libraries and device nodes from the host into the container, and leaves the CUDA toolkit in the image untouched.
The practical consequence is that the container's CUDA version must be supported by the host driver, but you do not install the driver inside the container. Never try to.
Prerequisites
The host driver must already be working:
nvidia-smi
If that fails, fix it before touching Docker — no container configuration compensates for a missing or broken host driver.
Installing the Container Toolkit
Debian and Ubuntu
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -sL 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
RHEL, CentOS, and Fedora
curl -sL https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo \
| sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo
sudo dnf install -y nvidia-container-toolkit
Warning
Many guides still use nvidia.github.io/nvidia-docker/ with apt-key add. Both are
obsolete: the repository moved to libnvidia-container, and apt-key has been removed
from current Debian and Ubuntu releases. The nvidia-docker2 package and the
nvidia-docker wrapper command are likewise superseded — --gpus replaced them.
Configure the Docker Runtime
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
That writes the nvidia runtime into /etc/docker/daemon.json:
{
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"args": []
}
}
}
Confirm the daemon picked it up:
docker info | grep -i runtime
To make every container GPU-capable without passing --gpus, set the default runtime — see
Daemon Configuration:
{
"default-runtime": "nvidia"
}
default-runtime is one of the few options that applies on systemctl reload docker.
Verify
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
Seeing the same GPU table as on the host means the whole chain works.
Running GPU Containers
# All GPUs
docker run --rm --gpus all myimage
# A specific GPU by index
docker run --rm --gpus '"device=0"' myimage
# Several by index — note the nested quoting
docker run --rm --gpus '"device=0,1"' myimage
# By UUID, which survives reordering across reboots
docker run --rm --gpus '"device=GPU-4a2b1c3d-..."' myimage
# A count rather than specific devices
docker run --rm --gpus 2 myimage
# Restrict capabilities
docker run --rm --gpus 'all,capabilities=compute,utility' myimage
The nested quoting on device= is a genuine requirement, not a typo — the shell must pass
the double quotes through to Docker.
# List UUIDs
nvidia-smi --query-gpu=index,name,uuid --format=csv
Environment Variables
The toolkit also reads these, which is how most CUDA images are configured:
| Variable | Purpose |
|---|---|
NVIDIA_VISIBLE_DEVICES |
all, none, indices, or UUIDs |
NVIDIA_DRIVER_CAPABILITIES |
compute,utility, video, graphics, or all |
NVIDIA_REQUIRE_CUDA |
Driver version constraint, e.g. cuda>=12.0 |
docker run --rm \
-e NVIDIA_VISIBLE_DEVICES=0 \
-e NVIDIA_DRIVER_CAPABILITIES=compute,utility,video \
myimage
video is required for NVENC/NVDEC hardware transcoding; compute alone will not expose the
encoder.
GPUs in Compose
Two forms work. The newer gpus key is simpler:
services:
inference:
image: myapp:latest
gpus: all
services:
inference:
image: myapp:latest
gpus:
- driver: nvidia
device_ids: ["0"]
capabilities: [gpu]
The older form nests under deploy, and is still what most examples show:
services:
inference:
image: myapp:latest
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
Note
deploy.resources is one of the deploy keys that docker compose up does honor, so
this works outside Swarm despite living under deploy. See
Compose Differences for which deploy keys are and are not
applied.
Use count: all or device_ids — not both. count and specific IDs are mutually exclusive.
Integrated and AMD Graphics
Intel QuickSync and AMD graphics need no vendor toolkit; the kernel driver exposes
/dev/dri and you pass it through directly. This is the common path for hardware video
transcoding in Jellyfin, Plex, and Frigate.
docker run -d \
--device /dev/dri:/dev/dri \
--group-add "$(getent group render | cut -d: -f3)" \
jellyfin/jellyfin
services:
jellyfin:
image: jellyfin/jellyfin
devices:
- /dev/dri:/dev/dri
group_add:
- "989" # the host's "render" GID — check with: getent group render
Important
The permission problem here is the usual one. /dev/dri/renderD128 is owned by the
render group (or video on older distributions), and the container's user must be in a
group with that numeric GID. Group names do not carry across the boundary — see
Permissions and Ownership.
ls -l /dev/dri/
getent group render video
AMD ROCm compute workloads additionally need /dev/kfd:
docker run --rm \
--device /dev/kfd --device /dev/dri \
--group-add video \
--security-opt seccomp=unconfined \
rocm/pytorch
Other Host Devices
# Serial adapters — Zigbee, Z-Wave, UPS
docker run -d --device /dev/ttyUSB0:/dev/ttyUSB0 zigbee2mqtt
# By stable path, since ttyUSB numbering changes across reboots
docker run -d --device /dev/serial/by-id/usb-Silicon_Labs_xxx:/dev/ttyUSB0 zigbee2mqtt
# TUN/TAP for VPN containers
docker run -d --device /dev/net/tun --cap-add NET_ADMIN vpnclient
# Restrict permissions: read, write, mknod
docker run -d --device /dev/sdb:/dev/sdb:r myapp
services:
zigbee2mqtt:
image: koenkk/zigbee2mqtt
devices:
- /dev/serial/by-id/usb-Silicon_Labs_xxx:/dev/ttyUSB0
Tip
Always reference USB devices by /dev/serial/by-id/ rather than /dev/ttyUSB0. The
numbered names are assigned in enumeration order, so adding a second adapter — or simply
rebooting — can silently point your container at the wrong hardware.
--privileged also grants device access, but it grants everything else as well. Use
--device for the specific nodes you need; see
Container Security.
Rootless and Swarm
Rootless Docker can use GPUs, but needs extra configuration: set
no-cgroups = true in /etc/nvidia-container-runtime/config.toml, because an unprivileged
daemon cannot manage the device cgroup. Expect rougher edges than rootful. See
Rootless Docker.
Swarm has no native GPU scheduling. The usual approach is a node label plus a placement
constraint, with --generic-resource for accounting:
deploy:
placement:
constraints:
- node.labels.gpu == true
See Placement Control. Workloads that genuinely need GPU scheduling are better served by Kubernetes with the NVIDIA device plugin.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
could not select device driver "" with capabilities: [[gpu]] |
Toolkit not installed, or runtime not configured — run nvidia-ctk runtime configure |
nvidia-smi works on host, fails in container |
Runtime not registered; check docker info \| grep -i runtime |
CUDA driver version is insufficient for CUDA runtime version |
Image's CUDA is newer than the host driver — upgrade the driver or use an older image tag |
Failed to initialize NVML: Unknown Error after some uptime |
Usually cgroups v2 plus systemd daemon-reload; set no-cgroups or pin the toolkit config |
No NVENC despite --gpus all |
Add video to NVIDIA_DRIVER_CAPABILITIES |
permission denied on /dev/dri/renderD128 |
Container user not in the host's render/video GID — use --group-add |
| Device disappears after reboot | /dev/ttyUSB* renumbered — use /dev/serial/by-id/ |
# What the toolkit thinks it can see
nvidia-ctk cdi list
# Full driver and device view from inside a container
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 \
bash -c 'nvidia-smi; ls -l /dev/nvidia*'
# Toolkit configuration
cat /etc/nvidia-container-runtime/config.toml
Related Topics
- Daemon Configuration — registering runtimes and setting
default-runtime - Working with Containers — resource limits and security options
- Docker Compose — declaring devices in a stack
- Rootless Docker — constraints on device access without root
- Container Security — why
--privilegedis not the answer - Local LLMs — Installation and Setup — GPU-accelerated model serving