身处无外网的隔离机房,对着服务器上那块明明好的 NVIDIA 显卡,却只能在 Docker 容器外干瞪眼——这个场景我在过去两年里反复遇到。容器一启动,直接报 could not select device driver "" with capabilities: [[gpu]],查遍资料发现核心问题就是缺了 nvidia-container-toolkit,而离线环境安装它,偏偏不像在线环境加个 apt 源、yum 源那么简单。
这篇文章就围绕 nvidia-container-toolkit 离线安装这件事,从方案选型、离线包准备、deb/rpm 两系系统的具体安装步骤,到 Docker runtime 配置、GPU 容器验证,再到我踩过的各种坑,一次性讲清楚。适合正在内网部署 AI 推理环境、离线安装 Docker 后需要启用 GPU 的运维和算法工程师参考。
1. 离线安装的整体思路与方案选型
1.1 nvidia-container-toolkit 到底解决了什么问题
先理清一个概念:很多人在离线环境碰到的 GPU 不可用,并不是驱动没装好,而是 Docker 本身不知道怎么把宿主机上的 NVIDIA 驱动和 GPU 设备“介绍”给容器。从 Docker 19.03 开始,官方支持了 --gpus 参数,但底层真正干活的是一套叫 nvidia-container-toolkit 的组件。
这套工具的职责可以拆成三块:
- 设备发现:识别宿主机上的 GPU 型号、设备节点,比如
/dev/nvidia0、/dev/nvidiactl、/dev/nvidia-uvm这些,并映射进容器。 - 驱动挂载:把宿主机上的 NVIDIA 驱动相关库文件(libcuda.so、libnvidia-ml.so 等)注入到容器文件系统里。
- 运行时钩子:通过 Docker 的 runtime hook 机制,在容器启动前完成上述准备工作。
换句话说,宿主机驱动是“发动机”,nvidia-container-toolkit 是“变速箱”。只装驱动不装 toolkit,容器里永远看不到 GPU。
1.2 为什么离线环境安装特别容易卡壳
在线环境装这个工具,其实就两三条命令的事:
code复制curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
但离线环境的问题在于:没有外部网络,apt/yum 源全部失效;内网源又不一定有 NVIDIA 的仓库;就算有安装包,deb/rpm 依赖关系也可能把你卡住。更麻烦的是,很多内网环境不只是没有外网,连内网的软件源也只同步了一些常见基础包,和 GPU 沾边的组件基本为零。
另外还有一个容易忽略的点:国内有不少基于开源社区版二次开发的操作系统,比如各类麒麟系统、欧拉系统,它们的包管理体系和依赖库版本跟标准的 Debian/CentOS 有一定出入。网上很多教程是 Ubuntu 或 CentOS 的,照搬到这类系统上经常出现各种不兼容。
1.3 三条离线方案路线对比
针对离线安装,我实践下来主要有三条路线:
| 方案 | 操作复杂度 | 适用场景 | 主要风险 |
|---|---|---|---|
| 离线 deb/rpm 包手工安装 | 低 | 单机或少量机器,内网无统一源 | 依赖不满足时需要手动补齐 |
| 自建内网 apt/yum 仓库 | 中高 | 大批量服务器,需要标准化交付 | 前期搭建耗时,仓库维护成本 |
| 构建包含 toolkit 的容器镜像 | 中 | 应用本身已容器化,不依赖宿主机工具 | 需要提前规划镜像层次,灵活性差 |
对于绝大多数一次性部署场景,我强烈建议走第一个方案:直接下载好 deb/rpm 包,拿到内网里用 dpkg -i 或 rpm -ivh 安装。虽然有时候要手动解决依赖,但总体可控,而且排错路径清晰。等确认机器数量多、以后还可能要重复部署时,再考虑搭一个本地源。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 离线安装前要做好的三件事
2.1 确认系统架构与 Docker 版本
动手之前,先花两分钟把环境信息摸清楚。这一步看似简单,但很多人就是栽在架构不匹配上——比如下载了 x86_64 的安装包,结果机器是 ARM 架构(鲲鹏、飞腾等国产化平台很常见)。
在目标机器上依次执行以下命令:
bash复制# 查看系统发行版和版本号
cat /etc/os-release
# 查看内核架构
uname -m
# 查看 Docker 版本
docker version --format '{{.Server.Version}}'
# 查看 Docker 存储驱动等信息
docker info
架构方面注意区分:x86_64、aarch64、arm64 是不同的安装包;系统版本方面,Ubuntu 20.04 和 22.04 的 deb 包不通用,CentOS 7(el7)和 CentOS 8(el8)的 rpm 包也不通用。如果你在 Ubuntu 上装了 CentOS 的 rpm,或者反过来,包管理器会直接拒绝或者提示架构冲突。
Docker 版本方面,建议至少 19.03 以上。19.03 之前的 Docker 不支持 --gpus 参数,需要走老的 nvidia-docker2 方案,那是另一套打补丁式的做法,现在官方已经不再推荐。如果内网环境的 Docker 版本过老,建议先把 Docker 本身升上去,再处理 toolkit,否则后面验证环节会非常别扭。
2.2 确认宿主机 NVIDIA 驱动正常
toolkit 只是让容器能用 GPU,但宿主机驱动要是没起来,后面装什么都白搭。在安装 toolkit 之前,先跑一下:
bash复制nvidia-smi
正常情况下会显示 GPU 型号、驱动版本、CUDA 版本等信息,例如:
code复制+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.105.17 Driver Version: 525.105.17 CUDA Version: 12.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Tesla T4 Off | 00000000:00:07.0 Off | 0 |
+-------------------------------+----------------------+----------------------+
如果这条命令本身就报错,比如 command not found 或者 No devices were found,那先解决驱动问题,别急着装 toolkit。驱动安装属于另一个话题,但它和 toolkit 的关系是:先有驱动,后有 toolkit。
还有一点值得注意:驱动版本和容器内 CUDA 版本的关系并不是“必须一致”。容器里的 CUDA 运行库和宿主机驱动是解耦的,驱动只需满足 CUDA 版本的最低要求即可。比如驱动 525.x 支持 CUDA 12.0,那么容器里跑 CUDA 11.x 或者 12.0 的镜像都没问题,但不能跑需要更高版本驱动的 CUDA 镜像。
2.3 在联网机器上准备离线安装包
这一步是离线安装成功的关键。你需要在另一台能联网的机器上,把安装包完整下载好,然后拷贝到内网机器上。
以 Ubuntu/Debian 系为例,我常用的做法是先配置好官方源,然后只下载不安装:
bash复制curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
# 下载 nvidia-container-toolkit 及其依赖包到当前目录
cd /tmp/nvidia-offline/
apt-get download nvidia-container-toolkit
apt-get download libnvidia-container1
apt-get download libnvidia-container-tools
对于 CentOS/RHEL 系,需要先启用官方仓库,再用 yumdownloader 把 rpm 拉下来:
bash复制curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo
sudo yum install -y yum-utils
cd /tmp/nvidia-offline/
yumdownloader --resolve nvidia-container-toolkit
--resolve 参数比较重要,它会自动把依赖的 rpm 一起下载下来。不过要注意,它只会下载 yum 源里已有的依赖;如果有些依赖本机已经有更高版本,yumdownloader 可能不会单独下载。稳妥起见,下载完以后手动把所有 rpm 文件都拷贝到内网机器上。
提示:如果你用的操作系统是国产化的开放欧拉、麒麟这类系统,直接使用 CentOS 的 rpm 包通常可行(欧拉系兼容 el8/el9,麒麟 V10 兼容 el7/el8),但要注意个别依赖库名称和版本可能不一样,具体问题见第 5 章。
把安装包、依赖包全部拷到目标机器上之后,建议放在同一个目录下,比如 /root/nvidia-offline/,后面安装时好统一处理。
3. 核心操作:deb/rpm 离线包安装与 Docker 配置
3.1 deb 家族系统的安装流程(Ubuntu/Debian/麒麟等)
先把所有 deb 包放到同一个目录:
bash复制mkdir -p /root/nvidia-offline
cd /root/nvidia-offline
# 用 U 盘或 scp 把 deb 文件传上来,确认文件都在
ls -l
然后执行安装:
bash复制sudo dpkg -i *.deb
dpkg 不会像 apt 那样自动解析依赖,如果遇到依赖缺失,常见报错是:
code复制dpkg: dependency problems prevent configuration of nvidia-container-toolkit:
nvidia-container-toolkit depends on libnvidia-container-tools (>= 1.16.0); however:
Package libnvidia-container-tools is not installed.
这种情况先执行 sudo apt-get install -f 看能不能自动修;如果离线环境没有可用的 apt 源,install -f 也大概率无效,那就只能手动补依赖包。最常见的依赖是 libc6(glibc)、libseccomp2,如果系统自带的版本太低,需要下载对应版本覆盖安装——这一步风险和收益并存,操作前记得备份原库文件。
在麒麟系统中,还有可能遇到依赖 libnvidia-container1 版本冲突。解决办法是先卸载旧版本或者强制指定版本安装:
bash复制sudo dpkg -i libnvidia-container1_1.16.2-1_amd64.deb libnvidia-container-tools_1.16.2-1_amd64.deb nvidia-container-toolkit_1.16.2-1_amd64.deb
不建议在离线环境随意加 --force-all,万一把依赖体系搞乱了,后面排查起来成本更高。
3.2 rpm 家族系统的安装流程(CentOS/欧拉/Anolis 等)
在目标机器上执行:
bash复制cd /root/nvidia-offline
sudo rpm -ivh *.rpm
如果报依赖错误,可以用 --nodeps 强制忽略,但我不建议一上来就这么做。正确姿势是先看缺什么:
bash复制rpm -qpR nvidia-container-toolkit-*.rpm
这条命令会列出所有依赖项,然后逐条对照本机是否满足:
bash复制rpm -q libnvidia-container1 libnvidia-container-tools libc6 libseccomp2
强依赖有几个:libnvidia-container1、libnvidia-container-tools、libseccomp.so.2、libc.so.6。如果缺的是 libseccomp,而且系统版本比较老,比如 CentOS 7 上跑新版本 Docker,就可能遇到这个问题,需要手动下载新版 libseccomp 的 rpm 装上。
3.3 用 nvidia-ctk 配置 Docker runtime
安装完 toolkit 之后,还差最后一步:让 Docker 认识 nvidia 这个 runtime。新版本 toolkit 提供了一个非常方便的命令行工具 nvidia-ctk:
bash复制sudo nvidia-ctk runtime configure --runtime=docker
这条命令会自动修改 /etc/docker/daemon.json,加入 nvidia runtime 的配置,修改之后大概长这样:
json复制{
"runtimes": {
"nvidia": {
"args": [],
"path": "nvidia-container-runtime"
}
}
}
然后重启 Docker 让配置生效:
bash复制sudo systemctl restart docker
这里有一个容易忽略的细节:如果你用的不是普通 Docker,而是 containerd 作为 Kubernetes 容器运行时,配置方式不同。K8s 场景下需要改 /etc/containerd/config.toml:
bash复制sudo nvidia-ctk runtime configure --runtime=containerd
sudo systemctl restart containerd
如果同时用 containerd 和 Docker,两条命令都要执行,别偷懒只配一个。
3.4 手动改 daemon.json 的备选方案
有时候 nvidia-ctk 可能不在 PATH 里,或者说你用的 Docker 版本比较特殊,自动配置方式失效。这时可以手动编辑 /etc/docker/daemon.json:
json复制{
"default-runtime": "nvidia",
"runtimes": {
"nvidia": {
"path": "/usr/bin/nvidia-container-runtime",
"runtimeArgs": []
}
}
}
注意:default-runtime 设为 nvidia 之后,所有容器默认带 GPU,这不一定是你想要的。更常见的做法是不设置 default-runtime,而是在运行容器时显式加 --gpus all 或者 --runtime=nvidia。我认为手动配置时,保持 default-runtime 为空更安全,避免把某些不需要 GPU 的服务也挂在 GPU runtime 上,引发不必要的资源占用。
改完之后,需要验证配置语法是否正确:
bash复制sudo docker info | grep -i runtime
能看到类似输出就说明 Docker 已经加载了 nvidia runtime:
code复制 Runtimes: nvidia runc
runc 是 Docker 默认的运行时,nvidia 是刚加进去的。如果这一行里没有 nvidia,说明 daemon.json 配置有问题,检查路径和 JSON 格式。还有就是改完 daemon.json 以后一定要重启 Docker,systemctl reload docker 不会重新加载 runtime 配置,这是个经常被忽略的坑。
4. 验证环节:确认 GPU 容器真正可用
4.1 三步基础验证
装完以后,别急着催网上下镜像跑业务,先做三层验证。
第一层:确认 toolkit 本身安装正确。
bash复制nvidia-container-cli info
这条命令会检测宿主机驱动、CUDA 库和 GPU 设备。正常输出会显示驱动版本、CUDA 版本、GPU 型号等信息。如果这里就报错,比如找不到驱动或库文件,说明 toolkit 和驱动的兼容性可能有问题,或者 libnvidia-container 库没装好。
第二层:确认 Docker 能识别 nvidia runtime。
bash复制sudo docker info | grep -A 5 Runtimes
第三层:用宿主机驱动直接验证 GPU 状态。
bash复制nvidia-smi
这一步和上一步的区别在于:nvidia-smi 是宿主机视角,Docker 配置是容器视角,两个都通过才是完整的链路。
4.2 跑一个 CUDA 容器实测
下载一个最小的 CUDA 基础镜像来测试,注意先确认内网的镜像仓库里有没有这个镜像,如果没有,就在联网机器上 docker pull 后 docker save 成 tar 包带进来。
bash复制# 在联网机器上
docker pull nvidia/cuda:12.2.0-base-ubuntu22.04
docker save nvidia/cuda:12.2.0-base-ubuntu22.04 -o cuda-base.tar
# 到内网机器上
docker load -i cuda-base.tar
然后运行测试容器:
bash复制sudo docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi
能看到 GPU 信息列表,说明链路完全打通。这里 --gpus all 后面的命令是 nvidia-smi,这个命令在镜像里是找不到的——等等,这个问题要注意。nvidia/cuda 基础镜像里其实没有预装宿主机的 nvidia-smi,但这个测试能跑通的原因是 nvidia-container-toolkit 把宿主机的驱动库挂载进了容器,而 nvidia-smi 这个可执行文件通常不会被挂载。所以正确写法是:
bash复制sudo docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi
这条命令运行后如果报 nvidia-smi: not found,不一定是 toolkit 有问题,而是镜像里本身没有这个工具。真正需要看的是能不能输出 GPU 信息。我用过的大多数情况下是能正常执行的,因为 toolkit 的钩子会把宿主机驱动库和工具链一起注入。如果你遇到 not found,可以用以下命令强制验证:
bash复制sudo docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 bash -c "ls /usr/local/nvidia/lib64 && cat /proc/driver/nvidia/version"
只要能列出 libcuda.so 等库文件,就说明挂载正常。
4.3 多卡环境与多容器并发下的坑
如果是多卡机器,--gpus all 会把所有 GPU 都暴露给容器。有些时候反而不该这么用——比如你只是想跑一个单卡任务。指定单卡的方式:
bash复制sudo docker run --rm --gpus '"device=0"' nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi
多容器并发时也容易出问题,主要体现在显存和算力隔离上。Docker 不会自动帮你限制显存用量,几个默认 --gpus all 的容器可以把你整卡显存吃光。如果你要严格隔离,需要配合 NVIDIA MPS 或者 GPU 虚拟化方案,这不是今天文章的范围,但你在做容量评估时一定要把这一点算进去。常见的现象是第一个容器正常,第二个容器一启动就报 CUDA_ERROR_OUT_OF_MEMORY,原因就是显存已经被占满。
另一个问题是 UVM(Unified Virtual Memory)设备节点。某些驱动版本下,容器里 CUDA 程序尝试分配统一虚拟内存时会报错,需要确认 /dev/nvidia-uvm 设备存在:
bash复制ls -l /dev/nvidia-uvm
如果不存在,可以手动加载模块:
bash复制sudo modprobe nvidia-uvm
但更根本的原因在于驱动安装时没有正确生成设备节点,尤其在 MIG 设备场景中更常见。这种情况下,最简单的验证手段是重启机器,让系统重新创建设备节点。
5. 踩坑实录与排查技巧速查表
5.1 最常见的五类报错与处理
5.1.1 could not select device driver "" with capabilities: [[gpu]]
这是没装 toolkit 或者没配置 runtime 时的典型报错。Docker 收到了 --gpus 参数,但找不到对应的驱动能力。解决办法:确认 nvidia-container-toolkit 是否安装,确认 daemon.json 中是否配置了 nvidia runtime,重启 Docker。
5.1.2 Unknown runtime specified nvidia
Docker 表示不认 nvidia 这个 runtime。原因通常是 /etc/docker/daemon.json 写错或者路径不对。检查 JSON 语法,检查 /usr/bin/nvidia-container-runtime 是否存在:
bash复制ls -l /usr/bin/nvidia-container-runtime
5.1.3 nvidia-container-cli: could not select device driver
toolkit 能执行,但找不到驱动。优先确认宿主机 nvidia-smi 是否正常,再看驱动是否被卸载或者内核模块未加载:
bash复制lsmod | grep nvidia
如果没有任何输出,说明驱动模块根本没加载,先解决驱动问题。
5.1.4 Error response from daemon: failed to create shim task: OCI runtime create failed
运行容器时底层 OCI 创建失败,原因可能是容器镜像和驱动不兼容。比如在太老的驱动上运行要求过高 CUDA 版本的镜像。此时可以先降低镜像 CUDA 版本验证,比如从 cuda:12.2 换成 cuda:11.4。
5.1.5 libcuda.so.1: cannot open shared object file
容器里能识别 GPU,但程序运行找不到 CUDA 库。通常是挂载路径问题。检查 --gpus 参数是否正确,或者改用 --gpus all 再试。如果自定义了 NVIDIA_VISIBLE_DEVICES 环境变量,检查是否误设成 void 或者不存在的设备编号。
5.2 快速排查命令清单
| 排查方向 | 命令 | 期望结果 |
|---|---|---|
| 驱动是否正常 | nvidia-smi |
GPU 列表与驱动版本正常 |
| 内核模块是否加载 | lsmod | grep nvidia |
有 nvidia、nvidia-uvm 等模块 |
| toolkit 是否安装 | dpkg -l | grep nvidia-container 或 rpm -qa | grep nvidia-container |
有相关包 |
| runtime 是否生效 | docker info | grep -i runtime |
有 nvidia |
| 设备节点是否正常 | ls -l /dev/nvidia* |
有 nvidia0、nvidiactl、nvidia-uvm |
| 容器内是否识别 GPU | docker run --rm --gpus all <镜像> nvidia-smi |
有 GPU 信息 |
这一套组合打下来,基本上能定位到是驱动、toolkit、Docker 配置还是容器镜像的问题。我的经验是:先看 /dev/nvidia* 和 nvidia-smi,再查 Docker runtime,最后才查容器内进程,大多时候问题都出在前两层。
5.3 我的离线环境工作习惯
踩了这么多坑之后,我总结了一些离线环境下的个人习惯。
一是在联网机器上准备离线包时,不光下载 toolkit 本身,顺手把所有依赖也一起下好。apt-get download 有时候不会自动拿全依赖,我一般会多跑一条 apt-cache depends 把依赖树列出来,手动核对一遍。
二是安装时把同一个软件的所有安装包放在同一个目录里,按系统版本分目录存放,比如 el7/、el8/、ubuntu20.04/、ubuntu22.04/。这看起来是个小细节,但机器一多,这个目录结构能帮你省下大量重复排查的时间。
三是装完 toolkit 后先不配 runtime,而是一个一个组的验证。先跑 nvidia-ctk runtime configure,重启 Docker,再拿一个基础镜像测试。这样如果后面出问题,你清楚地知道是哪一步引入的,不会陷入“一堆操作之后完全不知道从哪里排查”的困境。
四是把整个离线安装的 tar 包和文档留在服务器上。内网环境通外网不方便,但机器之间传输文件是很容易的,把工具包固化下来以后,新机器部署直接拷贝复用,省去重新找包、重新匹配版本的时间。
最后分享一个小技巧:离线环境下容器镜像往往也是稀缺资源,建议在一台能内网访问的机器上建一个本地 Docker Registry,把常用的 CUDA 基础镜像和业务镜像都推上去。这样不管 toolkit 装了多少台机器,镜像拉取永远不会成为瓶颈。把工具链和镜像都准备好,内网 GPU 环境的初始化就是一个标准的脚本化流程了。
