
有过内网 GPU 服务器部署经验的兄弟应该都体会过那种感觉驱动装好了、Docker 装好了、镜像也想办法导入到私有仓库了结果docker run --gpus all一跑直接给你弹一个unknown runtime。这台机器跟外网之间隔着网闸U 盘是唯一的搬运通道你根本不可能现场执行apt install或者yum install。问题其实不在 Docker也不在驱动而是缺了 nvidia-docker2 这套容器和 GPU 之间的桥梁。放到联网环境里一条命令就能解决的事到了离线内网就变成了下载依赖包、核对架构版本、手动按序安装的体力活。这篇文章把我最近一次完整离线部署 nvidia-container-toolkit 的过程拆给你看包括物料下载清单、安装顺序、Docker runtime 配置以及我在实际排查中踩到的几个典型坑。适合需要在没有外网的 GPU 服务器上启用 Docker GPU 容器的运维同学、算法工程师和交付工程师参考。1. 先说清楚nvidia-docker2 到底在容器和 GPU 之间扮演什么角色1.1 为什么容器默认用不了 GPU很多人刚接触这个问题时会很困惑宿主机上nvidia-smi跑得好好的驱动版本也正常为什么一到容器里就什么设备都看不到原因在于 Docker 容器默认的设备隔离方式。启动普通容器时/dev/nvidia0、/dev/nvidiactl、/dev/nvidia-modeset这些设备节点不会自动出现在容器里/usr/lib/x86_64-linux-gnu/libcuda.so.1、libnvidia-ml.so.1这些驱动动态库也没有被映射进去。有朋友可能会说我用--device /dev/nvidia0把设备文件手动映射进去不就行了能解决一部分问题但解决不了全部。nvidia-smi命令本身也是要依赖驱动动态库的你光映射设备节点不把用户态的库和工具链一并注入容器里的 CUDA 程序照样起不来。而且每次驱动升级设备路径和库版本都会变手工映射的方式根本维护不住。所以业界才需要一套自动化的机制在容器启动前扫描宿主机的 NVIDIA 驱动环境把设备节点、动态库、二进制工具全部注入到容器的 OCI spec 里去这个机制就是 nvidia-container-toolkit 家族。1.2 nvidia-container-toolkit 的工作链路这套组件的前身叫 nvidia-docker2而现在的核心包叫 nvidia-container-toolkit。整个调用链路大概是这样的Docker 启动容器时如果带了--gpus参数或者当前容器的 runtime 指定为nvidiaDocker 会把启动请求交给nvidia-container-runtimenvidia-container-runtime是一个 OCI runtime它在容器启动的 prestart hook 阶段调用nvidia-container-clinvidia-container-cli会扫描宿主机上的 NVIDIA 驱动信息生成一份需要注入的设备与库清单最终把所有/dev/nvidia*设备、/dev/nvidia-caps/*以及libcuda.so、libnvidia-*.so等库文件追加进容器的 OCI 配置容器启动后就能直接看到 GPU 了。用生活里的场景类比Docker 是酒店前台GPU 是宴会厅默认情况下住客容器没有宴会厅的门禁卡。nvidia-container-runtime相当于一个贴心的管家在你入住时自动帮你办好门禁卡把走廊和权限全部准备好你只管刷脸进厅。这也是为什么很多时候docker run --gpus all报错锅并不在 Docker而在 runtime hook 没装好或者没被正确配置。1.3 新旧名词混用的坑nvidia-docker2、nvidia-container-toolkit、libnvidia-container 到底指什么离线安装时需要下载包很多老文档还停留在 nvidia-docker2 时代下载页面上的包名已经换了好几轮如果没搞懂这几个名字的关系很容易下错包或者漏掉依赖。对照关系整理如下包名角色定位安装时是否需要nvidia-docker2早期的 meta 包安装后自动配置 Docker runtime旧项目还在用新项目不推荐nvidia-container-toolkit现在的主包包含 nvidia-container-runtime、nvidia-ctk 等推荐直接装这个nvidia-container-toolkit-base基础包包含部分公共文件通常会被主包依赖需一起装libnvidia-container1 / libnvidia-container-tools底层运行时库和 CLI实际干活的 nvidia-container-cli 就在这里必须装nvidia-container-runtimeOCI runtime 实现早期单独分发新版本已合并进 toolkit不必单独装简单说你最终要的其实是libnvidia-container的底层能力而nvidia-container-toolkit把这部分能力接给了 Docker。理解这层关系后离线下载时就不会面对一长串名字心里发慌了。2. 离线物料准备在一台联网机器上把安装包一次凑齐2.1 确认目标机器发行版、架构与驱动状态下载物料之前一定要先确认目标机器的身份。别笑这一步最容易被跳过结果到了离线环境里安装时才发现架构不对或者源版本不匹配。建议先执行以下几组命令并记录输出cat /etc/os-release dpkg --print-architecture # Ubuntu/Debian 用 arch # CentOS/RHEL 用 uname -r nvidia-smi --query-gpudriver_version --formatcsv docker version --format {{.Server.Version}}这里有几个关键点发行版版本影响你用哪个源目录比如ubuntu20.04、ubuntu22.04、rhel7.9、rhel8.4架构决定下载 amd64 还是 arm64 的包尤其是信创环境里跑 arm64 的机器特别多下错了包完全装不上驱动版本决定你要选哪个版本的 toolkit 包老驱动配新 runtime 大概率会报错。如果nvidia-smi在目标机器上都跑不起来先解决驱动安装问题再继续。toolkit 只是桥梁不是驱动桥搭得再好对岸的路没修好还是白搭。2.2 Ubuntu/Debian 下载 deb 包的完整过程准备一台和离线目标机同发行版、同架构、有外网的机器。我一般会开一台同版本的临时虚拟机这样下载的依赖体系最精确。先添加 NVIDIA 官方源并更新索引curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg echo deb [signed-by/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://nvidia.github.io/libnvidia-container/stable/ubuntu20.04/$(dpkg --print-architecture) / | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt-get update然后在一个干净目录里下载主包及其依赖。通常我直接下载这几个包mkdir /tmp/nv-docker-debs cd /tmp/nv-docker-debs apt-get download nvidia-container-toolkit nvidia-container-toolkit-base libnvidia-container1 libnvidia-container-tools下载之前建议先用apt-cache depends nvidia-container-toolkit看一下依赖关系因为不同版本、不同发行版下依赖包的名称可能略有差异。比如有的源里底层库叫libnvidia-container0而新版本叫libnvidia-container1用apt-cache search libnvidia-container扫一遍最保险。还有一个更省事的方法apt-get install --download-only nvidia-container-toolkit它会帮你把依赖全部解析好并下载到/var/cache/apt/archives/你直接从里面拷贝 deb 包走就行。这个方法唯一的缺点是需要一台真正能安装这个包的临时机器但在一台一次性虚拟机上完全没问题。2.3 CentOS/RHEL 使用 yumdownloader 下载 rpmRed Hat 系离线环境同样需要在联网机器上下载 rpm。首先要安装yum-utils工具yum install -y yum-utils添加 NVIDIA 官方仓库注意选择对应的 EL 版本目录yum-config-manager --add-repo https://nvidia.github.io/libnvidia-container/rhel8.4/libnvidia-container.repo然后下载主包和所有依赖mkdir /tmp/nv-docker-rpms cd /tmp/nv-docker-rpms yumdownloader --resolve --destdir/tmp/nv-docker-rpms nvidia-container-toolkit--resolve参数会分析依赖树并一起下载这是和 deb 系相比最大的优势。但要注意如果这台联网机器上配置了多个源下载出来的 rpm 可能混入其他仓库的依赖包最好用rpm -qp --queryformat %{NAME} %{VERSION} %{RELEASE}\n *.rpm核对一遍。2.4 别忘了带上一份验证用的 CUDA 镜像离线环境里拉不了镜像这一步经常被忽略。等到所有组件都装完了想验证一下 GPU 是否进容器结果docker pull nvidia/cuda拉不下来现场就尴尬了。在联网机器上预先准备一个验证用的基础镜像docker pull nvidia/cuda:11.8.0-base-ubuntu20.04 docker save nvidia/cuda:11.8.0-base-ubuntu20.04 -o /tmp/nv-docker-debs/cuda-base.tar选哪个镜像版本有讲究。验证 GPU 可见性用base系列就够体积小传输方便。镜像内的 CUDA 版本不需要和宿主机驱动版本严格一致只要驱动版本等于或高于镜像所需的最低版本就行。如果业务还在用 CUDA 12就拉nvidia/cuda:12.2.0-base-ubuntu20.04视你的实际场景决定。2.5 建议在物料包里一并放入的东西以下是我每次去离线机房前都会确认的清单deb 或 rpm 安装包以及对应的依赖包cuda-base.tar镜像文件docker-compose 二进制如果用户的业务用 compose 编排离线环境同样装不了一个写满安装步骤和命令的 README防止现场手忙脚乱如果可能带上一份目标机器原始环境的dpkg -l或rpm -qa快照。别嫌我啰嗦离线部署最怕的不是技术难而是缺料。一次准备充分到了现场半小时搞定准备不充分一个依赖缺失就能卡一下午。3. 目标机器离线安装dpkg/rpm 的依赖顺序与操作细节3.1 Ubuntu/Debian 手动按序安装 deb把下载好的 deb 包通过 U 盘或者其他合规转移方式拷到目标机器的/tmp/nv-docker-debs目录。安装时我建议不要直接dpkg -i *.deb因为dpkg会按字母序逐个安装遇到依赖不满足会一次性报错虽然可以事后补装但排查起来绕弯子。手动指定顺序更稳妥cd /tmp/nv-docker-debs sudo dpkg -i libnvidia-container1*.deb libnvidia-container-tools*.deb nvidia-container-toolkit-base*.deb nvidia-container-toolkit*.deb如果有额外的系统依赖包比如libseccomp2、libcap2版本过旧先把这些也放在同一目录里排在最前面安装。这里有个经验如果目标机器之前装过旧版本的 nvidia-container-toolkit升级安装时必须先sudo dpkg -r nvidia-container-toolkit否则dpkg会报告包冲突残留的老配置文件还会干扰新版本的 runtime hook。安装完成后验证关键二进制是否存在dpkg -l | grep nvidia-container which nvidia-container-runtime which nvidia-ctknvidia-ctk这个工具后面配置 Docker runtime 时会用到你下载的 deb 版本如果比较老可能没有这个命令那就需要手动改daemon.json我会在后面讲备选方案。3.2 CentOS/RHEL 使用 rpm 安装Red Hat 系目标机器上把下载的 rpm 放到同一个目录后执行cd /tmp/nv-docker-rpms sudo rpm -Uvh *.rpm或者用yum localinstall *.rpm后者会自动尝试解析本地的依赖关系比裸rpm -ivh体验好一些。注意 CentOS 8 以后yum和dnf是同一套实现命令通用。安装完成后同样验证rpm -qa | grep nvidia-container which nvidia-container-runtime which nvidia-ctk3.3 用 nvidia-container-cli info 做第一轮健康检查这是整个离线安装流程里我最看重的验证步骤甚至比 Docker 配置还靠前。很多文档都不会强调它但我在实际项目里发现nvidia-container-cli info能提前暴露至少一半的问题。sudo nvidia-container-cli info如果输出里能看到Driver Version、CUDA Version说明底层运行时库和宿主机驱动之间的通道是通的后面 Docker 配置即便出错排查范围也能大幅缩小。常见的报错也不用慌大致分两类nvidia-container-cli: mount error说明内核模块加载异常检查lsmod | grep nvidia确认模块是否正常加载could not select device driver则多半是权限或者 cgroup 配置问题需要继续往下查。如果在跑这个命令时就报一堆错先别急着配 Docker把底层问题解决掉否则配置了 daemon.json 重启 Docker 后照样起不了容器。4. Docker 侧配置让容器启动时自动注入 GPU 能力4.1 检查 Docker 版本与 runtime 组件Docker 对 GPU 的原生支持从 19.03 开始这个版本以后--gpus参数才可用。老版本 Docker 走的是 nvidia-docker2 时代的--runtimenvidia方式也能用但体验明显不如新参数方便。docker version --format {{.Server.Version}}Docker 的 runtime 配置逻辑很简单容器启动时Docker 根据daemon.json中定义的 runtime 把请求转交给对应的 OCI runtime。我们做的所有配置本质上是把nvidia-container-runtime注册给 Docker并告诉它默认使用这个 runtime 或允许通过参数指定。4.2 用 nvidia-ctk 自动配置 daemon.json新版 toolkit 提供了nvidia-ctk命令配置过程被简化成了两步sudo nvidia-ctk runtime configure --runtimedocker sudo systemctl restart docker第一个命令会自动修改/etc/docker/daemon.json写入default-runtime和runtimes字段同时生成一个备份文件daemon.json.backup。哪怕你后面把配置改坏了也能靠这个备份快速恢复。配置完检查一下docker info | grep -i default runtime输出里出现nvidia字样说明 Docker 已经默认走 NVIDIA runtime 了。这一步是离线部署成功的标志性时刻。4.3 手动编写 daemon.json 的备选方案如果你下载的是旧版本 toolkit没有nvidia-ctk命令或者你更习惯手动控制也可以直接编辑/etc/docker/daemon.json{ default-runtime: nvidia, runtimes: { nvidia: { path: /usr/bin/nvidia-container-runtime, runtimeArgs: [] } } }改完 JSON 后一定要先校验语法我见过不少同事在这里翻车少一个逗号、多一个花括号Docker daemon 直接起不来。校验方式docker version如果 daemon 没起来运行journalctl -u docker看日志十有八九是 JSON 解析失败。改回备份或者修正语法再systemctl restart docker。default-runtime这个字段要不要设取决于你的业务。设置了以后所有容器默认走 NVIDIA runtime即使不写--gpus也能访问 GPU。如果业务上有 GPU 资源隔离的需求更推荐不设默认值改成在需要 GPU 的容器启动参数里显式指定--gpus all。4.4 跑通第一个 GPU 容器验证命令配置完成后先把镜像导进来docker load -i /tmp/nv-docker-debs/cuda-base.tar然后执行验证命令docker run --rm --gpus all nvidia/cuda:11.8.0-base-ubuntu20.04 nvidia-smi如果能看到类似宿主机上nvidia-smi的输出说明整条链路已经通了。我再补充一个稍微严格一点的验证docker run --rm --gpus device0 nvidia/cuda:11.8.0-base-ubuntu20.04 nvidia-smi指定具体设备验证能确认设备索引映射是否正确特别适合后续要跑多卡服务的场景。有一点需要说明容器里执行nvidia-smi时用的其实是从宿主机注入进去的二进制和动态库镜像本身并不需要额外安装 NVIDIA 驱动。所以只要你容器内能正常跑nvidia-smi就说明这个容器的 GPU 访问链路是完全通的。5. 离线部署常见的几类坑与对应排查链路5.1 dpkg 报依赖未满足按顺序装也装不上这个场景我至少遇到过三次。现象是安装libnvidia-container1时提示需要更高版本的libc6或者libseccomp2而离线目标机的系统版本已经很老。排查时先在联网机器上确认目标机的基础依赖版本apt-cache depends libnvidia-container1然后把涉及的依赖包一并下载下来带到现场优先安装系统依赖再装 NVIDIA 相关包。如果目标机的 glibc 实在太老比如 CentOS 7 这种老环境就需要考虑换用旧版 toolkit。根据我的经验老驱动配新 toolkit 是兼容性问题的高发区尽量保持驱动版本和 toolkit 版本的时间跨度不要太大。5.2 docker run --gpus all 报 unknown runtime典型的报错是docker: Error response from daemon: unknown or invalid runtime name: nvidia原因就两个要么daemon.json里没配runtimes要么配了但 Docker 没重启生效。排查链路很简单cat /etc/docker/daemon.json docker info | grep -i runtime只要配置文件和 docker info 的输出对齐基本就能定位。处理方式也别无他法重新执行nvidia-ctk runtime configure --runtimedocker再systemctl restart docker。这个坑属于配置遗漏型只要做了docker info检查就不会漏。5.3 容器内 nvidia-smi 报驱动版本不匹配现象是容器正常启动了但执行nvidia-smi时报Failed to initialize NVML: Driver/library version mismatch这个问题经常被误判为 toolkit 没装好实际上和 toolkit 没关系。它通常发生在宿主机驱动升级或者降级之后没有重启的情况下内核里的驱动模块和用户态驱动库版本对不上。排查方式nvidia-smi lsmod | grep nvidia如果宿主机上nvidia-smi本身都报错那就不是容器的问题直接重启宿主机或者卸载驱动重装匹配版本。离线环境里重启机器代价高所以升级驱动前最好确认好版本再动手。5.4 SELinux 和权限导致的设备挂载失败在 RHEL/CentOS 系系统上安装好 toolkit 之后容器还是可能出现启动失败日志里经常出现failed to create task for container: failed to create shim task: OCI runtime create failed: error running hook: exit status 1这个时候用journalctl -u docker看详细错误如果里面出现Permission denied或设备节点挂载失败优先怀疑 SELinux。最快的定位手法是在测试机上临时关闭 SELinuxsetenforce 0如果容器能正常启动那就是 SELinux 策略拦截了设备挂载。生产环境不建议直接关闭 SELinux可以给容器加--privileged测试或者调整对应的 SELinux 布尔值。另外也检查一下/dev/nvidia*设备节点的权限有些发行版的 udev 规则不全设备节点没有 666 权限手动chmod可以临时解决根治还是要补 udev 规则。5.5 架构版本混用收到过不少朋友说离线安装时报wrong architecture amd64一看目标机器是 arm64。下载物料时没有确认架构尤其在混合架构机房里面特别容易出这种问题。处理方式很简单下载前确认dpkg --print-architecture或arch下载时指定一致的架构目录。现在信创环境里 arm64 的机器很多下载页面一般都会有arm64子目录别只盯着 x86_64 的源地址。5.6 离线机没有 apt lists误跑 apt install -fdeb 系安装时一旦dpkg -i报错很多人习惯性地想跑apt-get install -f来修复但在离线机器上这等于让 apt 去不存在的软件源里找东西轻则报Unable to locate package重则挂在那里半天没反应。正确的做法是离线机器上永远不要执行apt install -f除非你已经搭好了本地 apt 源。老老实实把所有依赖包放到同一个目录用dpkg -i重新按序安装依赖齐了就不会有问题。如果离线批量部署的场景很多可以考虑在目标机器里做一个私有 APT 源把Packages索引文件用dpkg-scanpackages生成好之后所有依赖都用apt正常解析省心很多。5.7 docker-compose 场景下 GPU 参数怎么传内网应用经常用 docker-compose 编排服务这种情况下 GPU 参数不能只写在docker run里。离线环境还得先把 docker-compose 二进制也带进去。新版 compose v2docker compose的写法services: cuda-app: image: nvidia/cuda:11.8.0-base-ubuntu20.04 command: nvidia-smi deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu]旧版 compose v1docker-compose用的是services: cuda-app: image: nvidia/cuda:11.8.0-base-ubuntu20.04 command: nvidia-smi runtime: nvidia这两个写法差别很大如果你带过去的 docker-compose 版本和业务配置文件对不上现场一跑就报 unknown runtime。所以离线物料里确认 docker-compose 版本和业务编排文件的兼容性也是必不可少的一步。5.8 我在离线批量部署前必做的一件小事讲完这些坑最后分享一个我自己的习惯。每次接到需要离线部署的 GPU 环境需求时我不会直接带着包杀到现场而是在联网环境里先开一台 Linux 虚拟机系统版本、内存、磁盘尽量模拟目标机器然后把整套离线安装流程完整走一遍。虚拟机里跑通了记录下每一步用到的命令和输出拿着这个经过验证的流程清点到现场执行。这套方法帮我躲过了很多坑。比如有一次我在模拟环境里发现目标机系统因为裁剪过缺了一个libcap2-bin包这个包在官方文档根本没有出现是在nvidia-container-cli info验证时才暴露出来的。提前在模拟环境发现这个问题比到了现场抓瞎强太多了。写在最后离线部署的三个经验总结如果你也是第一次做离线安装 nvidia-container-toolkit 这件事我的最后一条建议是越赶时间越要检查物料清单。我第一次去内网机房给 GPU 服务器装这套环境时只带了一个 toolkit 的 deb 包过去到了现场发现libnvidia-container1缺失机器又没有外网只能临时通过审批通道补传依赖包前前后后折腾了几个小时。后来我养成了一个习惯每次出发前对着物料清单过一遍安装包、依赖库、cuda 验证镜像、docker-compose 二进制、README 步骤一样不落。这套流程后来帮我在好几家单位的隔离网环境里完成过 AI 推理服务的容器化 GPU 部署基本没有翻过车。离线环境下的问题大多数不是技术复杂而是准备不充分。把物料备齐把验证步骤提前跑通剩下的就只是执行而已。