简介:面向 Jetson Nano 等 ARM64 平台的 Docker 部署入门文档,适合需要在嵌入式设备上搭建深度学习推理环境的开发者。内容围绕 docker 安装、nvidia-docker 运行时配置与 GPU 调用展开,从 apt 安装 docker-ce,到安装 nvidia-container-runtime,再到修改 daemon.json 将 nvidia 设为默认运行时,覆盖了镜像构建、容器启动时映射 /dev/nvhost-* 设备节点与 tegra 驱动目录等关键环节。文档整理为 1 个 docx 文件,共 208KB,按安装与部署顺序梳理,便于对照操作。已有 157 人浏览学习;相比网上零散的教程,这份资料把 Jetson 平台与 GPU 容器结合时容易踩坑的设备映射、库路径挂载等问题集中说明,并给出了可执行的 Dockerfile 示例;对于想快速跑通 nvidia-docker 并调用 GPU 的开发者,可以节省不少排查时间。文档中 deviceQuery 容器测试与 Dockerfile 基础镜像选择等细节,对有 CUDA 环境诉求的用户也很有参考价值;作者还提供了配套免费博文,方便进一步查阅迁移部署细节。
1. 在 Jetson Nano 上让 Docker 调用 GPU:为什么刚装好的 Docker 跑不了 CUDA 容器
很多人拿到 Jetson Nano 的第一反应是先装个 Docker,装完跑 hello-world 一切正常,接着docker run --gpus all nvidia/cuda直接报错 could not select device driver。这不是命令写错了,而是 Jetson 的 GPU 访问链路和 PC 完全不一样:Tegra 的驱动不在镜像里,CUDA 库在宿主机的 /usr/lib/aarch64-linux-gnu/tegra 下,GPU 设备则分散在一组 /dev/nvhost-* 节点里,缺一个容器就拿不到计算资源。这篇文章把我在 Jetson Nano 上从零走通的完整流程拆开讲:Docker 安装、NVIDIA Container Runtime 配置、daemon.json 默认运行时、Dockerfile 构建、设备节点挂载,最后是镜像迁移。每一步都给出能直接复制的命令,坑单独放在第五章,按现象找原因就行。这篇适合想在自己板子上跑 CUDA 容器、想把整套环境固化下来迁到别的 Jetson 设备上的读者,照着做基本一遍过。
2. 安装 Docker 与 NVIDIA Container Runtime:从 apt 源到运行时验证
Jetson 上的 JetPack 底层就是 Ubuntu,Docker 本体可以走官方 Ubuntu 安装流程,但有几个参数必须按 ARM 平台调整。我按实际操作的顺序写,每一步说明为什么这么写,遇到 404 或者依赖缺失也知道去哪查。
2.1 先装 Docker 本体:apt 源与架构参数一次配好
先把基础工具装齐,然后用官方 GPG 密钥配置 apt 源,最后安装 docker-ce 三件套。如果你之前用 apt 装过 docker 或 containerd,先sudo apt remove docker docker-engine docker.io containerd runc清干净,并把 /var/lib/docker 改名备份,避免升级残留冲突,这是从旧系统带着配置过来最容易翻车的地方。
sudo apt-get update sudo apt-get install -y \ apt-transport-https \ ca-certificates \ curl \ gnupg \ lsb-release curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \ sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo \ "deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io第一组 apt-get install 装的是 apt 走 HTTPS 和校验 GPG 所需的前置工具。curl 那行把 Docker 的公钥下载下来,用 gpg --dearmor 转成二进制放到 /usr/share/keyrings 下,这样 apt 校验源签名时不会弹 warning。注意输出文件名是 docker-archive-keyring.gpg,后面 apt 源里 signed-by 必须指向同一个文件,路径写错会直接导致后续 update 失败。
echo 那行写 apt 源,arch=arm64是本次安装最容易错的地方:写成 amd64 会在 apt update 时提示架构不匹配,什么都不写虽然也能装上,但 apt 会拿错架构的包列表。$(lsb_release -cs)自动取系统代号,JetPack 4.x 对应 Ubuntu 18.04(bionic),JetPack 5.x 对应 20.04(focal),写成变量后换系统版本不用改配置。
装完确认两件事:
docker --version sudo systemctl status docker如果 docker 命令存在但服务没起来,先看journalctl -u docker再继续,常见原因是 daemon.json 写错导致 dockerd 拒绝启动,或者和已存在的 containerd 版本冲突。顺手把当前用户加进 docker 组(sudo usermod -aG docker $USER),之后不用每条命令都带 sudo。这里比 PC 上省心的点在于:Jetson 走的是 Linux 原生 Docker 加 NVIDIA Container Runtime 的路线,不会遇到 Docker Desktop 在 PC 上常见的 virtualization support not detected 这类虚拟化层问题,少了一层排查。
2.2 安装 nvidia-container-toolkit 与 runtime:顺序反了会白折腾
x86 上很多人直接apt install nvidia-docker2就完事,Jetson 不行。Tegra 平台 GPU 驱动不是通过 PCIe 暴露的,nvidia-docker2 的 hook 在 Jetson 上找不到硬件,必须改用 nvidia-container-runtime 加 nvidia-container-toolkit 这套组合。执行:
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - distribution=$(. /etc/os-release;echo $ID$VERSION_ID) echo $distribution curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list sudo apt-get update sudo apt-get install -y nvidia-container-toolkit sudo apt-get install -y nvidia-container-runtime sudo systemctl restart dockerdistribution=$(...echo $ID$VERSION_ID)这行是关键,它会拼出 ubuntu18.04 或 ubuntu20.04 这样的字符串,下一行 curl 用这个字符串拼仓库路径。如果这里输出是空的,检查 /etc/os-release 是否存在,部分 JetPack 精简版系统会缺这个文件,手工填 distribution=ubuntu18.04 也能走通。
两个 apt install 的顺序建议固定:toolkit 里包含 nvidia-container-cli,runtime 依赖它做设备注入。先装 runtime 再装 toolkit 也能装,但 toolkit 装完会覆盖更新 runtime 的配置,等于白折腾一遍。装完不重启 Docker,runtime 不会生效,后面docker info里看不到 nvidia。我见过不少人卡在这一步,误以为没装成功,反复重装了好几轮。
如果仓库 404,可以单独下载 nvidia-container-runtime-hook 的 deb 包手工装:
wget https://nvidia.github.io/nvidia-container-runtime/ubuntu14.04/amd64/./nvidia-container-runtime-hook_1.4.0-1_amd64.deb sudo dpkg -i nvidia-container-runtime-hook_1.4.0-1_amd64.deb sudo apt-get -f install注意这里包的发布路径是上游仓库的遗留命名,里面带了 ubuntu14.04 和 amd64,实际上是个通用 hook 包,arm64 的 Jetson 系统装了也能正常用。dpkg 报依赖缺失就执行sudo apt-get -f install补齐再继续。
2.3 配置前的基线验证:docker info 与 deviceQuery 的预期输出
现在先别急着改配置文件,跑两条命令记录未配置状态,后面才有对比:
docker info | grep -i runtime docker run -it --rm jitteam/devicequery ./deviceQuery第一条命令,未配置时通常只显示 runc;如果显示 runc 和 nvidia 都出现,说明系统里已经存在旧配置,第 3 章的改动会被旧配置影响。第二条命令用 Jetson 社区常用的 jitteam/devicequery 镜像,里面打包了 NVIDIA 官方 deviceQuery 程序的交叉编译产物,专门用来验证容器能否访问 GPU。
关键点:第一次跑理想输出是Result=FAIL。这不是失败,而是基线——Docker 默认用 runc 启动容器,容器里没有任何 GPU 资源,deviceQuery 探测不到设备自然返回 FAIL。看到 FAIL 不需要重装驱动,只要容器能正常启动、能看到 CUDA 相关日志,就说明 Docker 本体没问题。更稳妥的验证方式是同时看容器的 exit code:如果 docker run 直接报错退出,说明容器没起来,问题在 Docker 侧;如果容器起来了、输出了 FAIL,问题只在 GPU 资源分配,这是第 3 章要解决的事。
提示:日志里出现 could not select device driver,说明 dockerd 的 runtimes 列表里还没有 nvidia,这是配置问题,不是驱动问题,别卸载重装。
3. 把 nvidia 设为默认运行时:daemon.json 两个字段与三次确认
装完 runtime 只完成了一半。Docker 默认还是用 runc 启动所有容器,必须显式告诉 dockerd nvidia 这个运行时怎么调、要不要作为默认。这一步不配置,GPU 容器永远跑不起来,而且报错非常误导人。
3.1 daemon.json 字段解读:default-runtime 与 runtimes 缺一不可
修改 /etc/docker/daemon.json,内容如下:
{ "default-runtime": "nvidia", "runtimes": { "nvidia": { "path": "nvidia-container-runtime", "runtimeArgs": [] } } }两个字段各管一件事。runtimes是注册表,告诉 dockerd 存在一个名为 nvidia 的运行时,它的可执行文件是 nvidia-container-runtime,runtimeArgs 留空数组表示不需要额外参数。default-runtime是全局开关,把 nvidia 设为默认后,以后所有 docker run 启动的容器都自动走 nvidia-container-runtime,不用每条命令都带--runtime nvidia。
我每次改这个文件前都会先备份:
sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bakdaemon.json 本身是可选的,如果之前不存在,直接新建。JSON 里不能有注释,不能有尾逗号,键名不能写错。最容易犯的低级错误是把 default-runtime 写进 runtimes 里面,或者把 runtimes 写成单数 runtime,这两处写错 dockerd 都会启动失败,错误信息却只是 generic 的 failed to start daemon,不仔细看 journalctl 根本定位不到。
改完后先做一次语法校验再重启:
sudo dockerd --config-file /etc/docker/daemon.json这条命令会前台启动 dockerd,如果 JSON 格式有问题,会直接打印出具体的解析错误行号。确认没有报错后 Ctrl+C 退出,再执行sudo systemctl restart docker。我习惯把这条验证写进操作清单,它能把配置错误和系统问题在 10 秒内分开。
3.2 改完配置的完整验证路径:重启、grep、再跑一次 deviceQuery
重启后按顺序做三次确认,缺一次都可能在后面埋雷。
sudo systemctl restart docker docker info | grep -i runtime正常情况下输出包含以下两行:
Runtimes: nvidia runc Default Runtime: nvidia如果 Runtimes 里有 nvidia 但 Default Runtime 不是 nvidia,说明 default-runtime 字段没写对,或者拼写和 runtimes 里的 key 不一致。如果 Runtimes 里压根没有 nvidia,说明 daemon.json 没被加载,检查文件路径和权限,/etc/docker/daemon.json 需要 root 可读,一般 644 权限没问题。
确认注册成功后,再跑一次 deviceQuery,这次显式指定运行时:
docker run -it --runtime nvidia jitteam/devicequery ./deviceQuery这次结果应该是Result=PASS,设备名称显示 Jetson 的 GPU 型号。跑完 PASS 后再用默认运行时跑一次不带 --runtime 参数的版本,确认 default-runtime 确实生效。两次都 PASS,GPU 容器链路才算真正打通,可以进入第 4 章构建自己的镜像。
3.3 为什么设成默认运行时:省掉的不仅是命令参数
有人会问:不设 default-runtime,每次手动加--runtime nvidia不也一样?表面看一样,实际有两个隐患。
第一个是环境变量丢失。nvidia-container-runtime 在启动容器时会根据宿主机注入 CUDA 相关环境变量,比如 CUDA_VERSION、NVIDIA_VISIBLE_DEVICES。如果忘记带--runtime nvidia,容器里即便有 CUDA 库也识别不到设备,报错还特别隐蔽,程序可能直接段错误。第二个是编排工具的兼容性。docker-compose 对 runtime 字段的支持在部分版本上不完整,compose 文件里没写 runtime 时,容器就会走默认的 runc,GPU 访问静默失败。设成默认 runtime 之后,所有容器不管怎么起都走同一个运行时,少踩很多坑。
但默认运行时不是万能的,它有明确的边界:daemon.json 只解决用哪个运行时启动容器,设备节点和 tegra 库的挂载仍然要自己在 docker run 或 compose 里写。换句话说,第 3 章解决的是运行时选择,第 4 章构建镜像,第 5 章的设备挂载解决的是容器里能看到哪些 GPU 资源,三件事分开理解,排查时才不会互相绕。
4. 用 Dockerfile 构建可用的 CUDA 镜像:基础镜像、依赖与 build 参数
运行时配好了,接下来要做的不是直接拉现成的 CUDA 镜像,而是构建一个符合 Jetson Tegra 环境的容器。很多 PC 上的 CUDA 镜像在 Jetson 上跑不起来,因为它们没有针对 Tegra 的库路径做处理。这一章从 Dockerfile 逐行拆解到 build 命令,把每个依赖为什么装说清楚。
4.1 Dockerfile 逐段拆解:基础镜像、时区与依赖
创建一个工作目录并新建 Dockerfile:
mkdir docker-test cd docker-test vi DockerfileDockerfile 内容如下,我按构建顺序逐段说明:
FROM arm64v8/ubuntu:16.04 ENV LD_LIBRARY=/usr/lib/aarch64-linux-gnu/tegra RUN mkdir /cudaSamples COPY deviceQuery /cudaSamples/ ENV LANG C.UTF-8 RUN apt-get update -y && apt-get upgrade -y RUN apt-get install -y tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN apt-get -y install vim RUN apt-get install -y libsqlite3-dev RUN apt-get -y install gcc RUN apt-get install libssl-dev -y RUN apt-get install make RUN apt-get -y install zlib* RUN apt-get -y install wget基础镜像选 arm64v8/ubuntu:16.04,这是 JetPack 4.x 时代最常见的系统基座,Tegra 的库文件路径按 aarch64 结构组织。ENV LD_LIBRARY这一行原文里写的是 LD_LIBRARY,实际作用等价于后续容器内的库搜索路径前缀,建议写成ENV LD_LIBRARY_PATH=/usr/lib/aarch64-linux-gnu/tegra,否则运行 deviceQuery 时动态链接器找不到 libcuda.so。
COPY deviceQuery 这行要求构建目录里必须存在一个名为 deviceQuery 的可执行文件,它是从 jitteam/devicequery 镜像里拷出来的,或者你在宿主机交叉编译得到的 arm64 版本。这个文件缺失时 docker build 会直接报 COPY failed: file not found。
后面几行按需安装:tzdata 配合ln -sf把时区固定到上海,避免后续 Python 或 OpenCV 运行时报 timezone 警告;libsqlite3-dev 是编译 Python 时 sqlite3 模块的依赖;gcc 和 make 是源码编译必需;libssl-dev 解决 Python 编译时 No module named _ssl 的问题;zlib* 是很多第三方库编译时的通用依赖。
这里有个优化点:可以把多个 RUN 合并成一个 RUN,用 && 连接,减少镜像层数。我一般会把依赖安装合并成一条,只有补充工具单独留一层,便于后续修改。上面的 Dockerfile 保持了原始风格的拆分,便于你看清每个包的作用,实际构建时可以按我的习惯合并。
4.2 docker build 构建:上下文目录与镜像命名
Dockerfile 写好后执行构建。这一步的关键不在命令本身,而在你执行命令时所在的目录:
docker build -t nano_env_img .命令最后的点代表构建上下文目录,docker 会把当前目录整个打包发给 dockerd。所以执行这行命令前必须确认已经在 docker-test 目录里,而且 deviceQuery 文件就在这个目录下,否则 COPY 指令找不到源文件。镜像名 nano_env_img 可以任意取,我习惯带项目名和日期,比如 jp46-ai-env-0215,方便后面迁移时辨认。
构建过程中如果 apt-get 走到一半卡住,多数是网络波动,直接重新跑一次 docker build 即可,Docker 会复用已经完成的层,不会从头再来,这个特性在慢速网络下特别好用。
构建完成后确认镜像存在:
docker images | grep nano_env_img docker run -it nano_env_img /bin/bash能进入容器说明基础构建通过。这一步先不挂设备节点,因为接下来第五章才讲运行容器的完整参数。
4.3 准备运行容器所需的路径清单
运行 GPU 容器前,先确认宿主机的设备节点和库目录都存在,顺手检查一下,别等 docker run 报错了再排查:
ls -l /dev/nvhost-* /dev/nvmap ls -l /usr/lib/aarch64-linux-gnu/tegra | head如果设备节点不存在,说明 JetPack 的 BSP 驱动没加载完全,后续挂载无从谈起;如果 tegra 目录为空,CUDA 运行库缺失,需要重新刷 JetPack 或者在宿主机装对应的 CUDA-arm 包。这里提一下 arm 版 CUDA 工具链,官方有单独的 aarch64 发布通道,Jetson 上的 CUDA 版本必须和 JetPack SDK 版本对齐,不能用 x86 的 CUDA 安装包替换。
这两个检查都通过后,第 5 章给出的运行命令才能真正生效。
5. 避坑:Jetson Nano GPU 容器最常见的 5 个翻车现场
这一章是把前面每一处的踩坑记录集中起来。每条按现象、原因、解决展开,你遇到哪个就直接定位到哪条。
5.1 现象:daemon.json 改完,Docker 服务起不来
sudo systemctl restart docker后,systemctl 提示 failed,docker info直接报 Cannot connect to the Docker daemon。查 journalctl -u docker 会看到一段 dockerd 启动失败的日志,但错误内容很泛,看不出具体原因。
原因基本就两个:daemon.json 的 JSON 语法错误,或者键名写错。最常见的是把 default-runtime 写进了 runtimes 对象内部,或者 runtimes 误写成 runtime,导致默认字段解析不到。
解决方法是先把配置备份,然后用sudo dockerd --config-file /etc/docker/daemon.json前台启动,错误信息会精确到第几行第几个字符。修好后再用 systemctl 启动,并重新执行docker info | grep -i runtime确认 Default Runtime 字段出现。
5.2 现象:容器能起来,但 deviceQuery 报找不到 libcuda 或 tegra 相关库
容器能进入 shell,但执行 ./deviceQuery 时提示error while loading shared libraries: libcuda.so.1: cannot open shared object file。
原因是在 Jetson 上,GPU 运行库不在镜像里,而在宿主机的 /usr/lib/aarch64-linux-gnu/tegra 目录下。单靠 nvidia-container-runtime 不会自动挂载这个目录,必须手动通过 -v 参数映射到容器内相同路径。
解决方式是在 docker run 里加上-v /usr/lib/aarch64-linux-gnu/tegra:/usr/lib/aarch64-linux-gnu/tegra,并在 Dockerfile 里显式设置ENV LD_LIBRARY_PATH=/usr/lib/aarch64-linux-gnu/tegra。只挂载目录但没设置环境变量,还是可能因为 link 解析不到而失败,两个动作一起做才稳。
5.3 现象:设备能识别,但 deviceQuery 最终 Result=FAIL
容器能启动,deviceQuery 也能找到显卡型号,但结果始终是 FAIL,或者报 CUDA-capable device 数量为 0。
原因是在容器启动时没有把 GPU 设备节点传进去。CUDA 在 Jetson 上访问 GPU 不依赖 /dev/nvidia0,而是依赖一组 Tegra 专属节点:/dev/nvhost-ctrl、/dev/nvhost-ctrl-gpu、/dev/nvhost-prof-gpu、/dev/nvmap、/dev/nvhost-gpu、/dev/nvhost-as-gpu。这些节点分别负责 GPU 控制、profiling、内存映射等,漏传任何一个都可能导致初始化失败。
解决方式是把六个节点全部用 --device 参数加进 docker run 命令。注意有些教程只传 nvhost-gpu 和 nvmap,结果 container 能跑起来但 CUDA 的 context 创建失败,因为缺了 nvhost-ctrl-gpu。完整的六个我列在第六章的运行命令里,直接复制即可。
5.4 现象:容器内编译 Python,报 No module named _ssl
在容器内源码编译 Python 3.6.5,make 顺利通过,但运行 python 后在 import ssl 时报 No module named _ssl,pip 也完全没法用,因为 pip 依赖 ssl 模块。
原因是编译 Python 前没有安装 OpenSSL 的开发头文件,configure 阶段检测不到 libssl,于是生成的 Python 里没有 _ssl 模块。如果你没有在 Dockerfile 里安装 libssl-dev,直接 ./configure && make,默认情况下 _ssl 是缺失的。
解决方式是在编译前先apt-get install libssl-dev -y,configure 时带--with-ssl,然后再 make install。如果已经编译完成,需要清理后重来:在源码目录执行make clean,补齐依赖后重新 configure 和 make。
5.5 现象:docker build 慢到怀疑人生,中途还报网络错误
在 Jetson Nano 上构建镜像,apt-get update 经常卡在某个奇怪的 1%,然后超时,或者 docker build 反复提示 unable to resolve host,构建进程断掉。
原因有两方面:一是小内存设备上 dockerd 和 build 进程资源竞争严重,二是镜像里的 apt 源在板子网络环境下延迟很高。另外如果你在容器里不断执行单个 RUN apt-get 装包,每一层都要重新解析依赖,慢上加慢。
解决方式是把多个 apt-get install 合并成一条 RUN,减少层数和解析次数;如果网络确实不稳定,适当增加 apt 的重试参数:
RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ libsqlite3-dev gcc libssl-dev make zlib1g-dev wget && \ rm -rf /var/lib/apt/lists/*--no-install-recommends 能少装很多无关推荐包,显著缩短构建时间。rm -rf 清理 apt 缓存,把镜像层体积压下来。这些改动不影响功能,但对 Jetson Nano 这种小内存设备的构建体验提升非常大。
6. 保存镜像并迁移到另一台 Jetson:docker commit 后的三个检查点
环境配置完毕,最划算的收尾方式是把容器固化成镜像,然后迁移到其他 Jetson 设备上复用。我的做法是先把容器内环境调通,再执行提交。
docker commit nano_test nano_env_img:v1 docker save -o nano_env_img_v1.tar nano_env_img:v1第一条命令把当前运行的容器 nano_test 保存成新镜像 nano_env_img:v1,里面对应的是你在容器内做的所有环境修改,包括手动编译的 Python、安装的依赖库。第二条命令把镜像导出成 tar 文件,拷贝到目标 Jetson 设备后用docker load -i nano_env_img_v1.tar导入。之所以用 docker save 而不是 docker export,是保留镜像的层结构和构建历史,后续还能基于它继续打补丁;docker export 打出来的是扁平文件系统,镜像元数据会丢掉。
镜像迁移到新设备后,有三件事必须重新检查,少一件都可能白拷一次:第一,新设备的 daemon.json 是否也配置了 nvidia 默认运行时,用docker info | grep -i Runtime确认 Default Runtime 为 nvidia;第二,新设备的/usr/lib/aarch64-linux-gnu/tegra 目录是否存在,不同 JetPack 版本的 tegra 库版本可能不同,如果新设备 JetPack 比构建时的版本新,建议在容器里重新跑一次 deviceQuery 验证;第三,运行命令里的六个 --device 节点和 -v 挂载一个都不能少。
最终运行命令我固定写成这样:
docker run -it \ --device=/dev/nvhost-ctrl \ --device=/dev/nvhost-ctrl-gpu \ --device=/dev/nvhost-prof-gpu \ --device=/dev/nvmap \ --device=/dev/nvhost-gpu \ --device=/dev/nvhost-as-gpu \ -v /usr/lib/aarch64-linux-gnu/tegra:/usr/lib/aarch64-linux-gnu/tegra \ --name nano_test nano_env_img:v1每条 --device 对应一个具体的 Tegra 硬件资源,-v 把宿主机驱动库映射进容器。迁移后如果结果从 PASS 变成 FAIL,优先比对这三项,而不是重新装 runtime。从那以后我每次换设备部署 Jetson 容器,都会强制走一遍这套检查流程:docker info 看 runtime、ls tegra 目录看驱动、跑 deviceQuery 看结果,三个检查做完才敢把环境交付出去。希望这份经验能帮你少走几趟弯路。
本文还有配套的精品资源,点击获取