关于 Docker 的基础知识见:FunnyWii's Zone Docker 基础知识
- 硬件设备:天准 Orin,基于 NVIDIA Jetson Orin AGX。
- 系统版本:Ubuntu 20.04。
- 需打包的库:CUDA、OpenCV。
值得一提的是,我的设备是 ARM 架构的。所以要注意一点:使用 Docker 构建镜像时,CPU 指令集不同会带来额外限制。尽管可以在 x86 设备上用下面的命令拉取 ARM 设备使用的镜像:
docker pull --platform=linux/arm64 <image>
但这不代表可以在 x86 设备上原生 docker run 或 docker build 这个 ARM 镜像。通常需要 QEMU / binfmt、Docker buildx,或者直接在 ARM 设备上构建。
修改 docker pull 的默认路径
老版 Orin 平台虽然挂载了一个 512 GB 的 SSD 硬盘,但是根目录所在硬盘只有 64 GB。对于动辄几个 GB 甚至几十个 GB 的镜像来说,这还是不够的,毕竟自己乱七八糟的库都占了十来个 G。
因此,在 pull 镜像之前,需要修改 Docker 的默认数据目录。默认路径为:
/var/lib/docker/
可以使用下面的命令查看当前 Docker 的默认路径。这里显示的是我已经修改成功后的结果:
$ docker info | grep "Root Dir"
Docker Root Dir: /media/nvidia/0739c4aa-df47-4f65-b632-6c50782120ab/CV/Fuse/dockerPath
最直接暴力的方法,是把硬盘直接挂载到这个目录下面,但是这会对文件管理造成困扰。
也可以采用软链接的方式。首先停止 Docker 服务:
sudo service docker stop
然后复制 /var/lib/docker/ 这个目录到新的 Docker 存储目录下:
sudo cp -a /var/lib/docker /media/nvidia/0739c4aa-df47-4f65-b632-6c50782120ab/CV/Fuse/dockerPath
备份原目录的数据:
sudo mv -u /var/lib/docker /var/lib/docker.bak
新建一个 /var/lib/docker/ 目录的软链接:
sudo ln -fs /media/nvidia/0739c4aa-df47-4f65-b632-6c50782120ab/CV/Fuse/dockerPath /var/lib/docker
最后重启 Docker 服务:
sudo service docker start
之后 docker pull 的时候,虽然 Docker 的写入路径仍然显示为 /var/lib/docker/,但由于软链接的原因,实际上是在向新的存储目录中写入。
不过更规范的做法,是修改 Docker daemon 的 data-root。例如编辑 /etc/docker/daemon.json:
{
"data-root": "/media/nvidia/0739c4aa-df47-4f65-b632-6c50782120ab/CV/Fuse/dockerPath"
}
然后迁移原始数据并重启 Docker。相比软链接,data-root 更清晰,也更容易排查问题。
commit 方法修改并保存 Jetson 官方镜像
Jetson 官方镜像的选择
在 dusty-nv/jetson-containers 或 Cloud-Native on Jetson 都可以找到 Jetson 相关 Docker 镜像。由于我需要使用视觉和 CUDA 相关环境,所以选择了 deepstream 的 Docker 镜像。
需要注意镜像版本和 L4T / JetPack 的对应关系。我的 JetPack 是 5.0.2 GA,对应 L4T 为 35.1.0。不过我直接下载了 deepstream-l4t:6.2-triton 镜像,使用起来并没有出现问题。因此接下来都以 6.2-triton 为例。
DeepStream 镜像共有 4 个版本:
- Base 版:包含 DeepStream SDK 一部分的插件、库以及依赖项,如 CUDA、TensorRT、GStreamer 等。希望为自己创建 Docker 版 DeepStream 应用程序的用户,建议使用此镜像。需要注意,这个镜像不包含示例内容。
- Samples 版:在 Base 版基础上添加范例内容,包括 C/C++ 开源代码、deepstream-app 范例配置文件、模型文件与测试视频等,适合体验与学习用途的初学者使用。
- IoT 版:在 Base 版上扩充 IoT 应用所需环境,包括 Kafka、Azure IoT、Redis 和 MQTT 等协议、DeepStream test5 应用程序以及相关配置和模型,可启用多视频流应用程序,并将各种消息传递到服务器端进行统计分析。
- Triton 部署版:配合 Triton 推理服务器使用的环境,开发者可以直接使用 TensorFlow、TensorFlow-TensorRT 与 ONNX-RT 等方式进行推理计算。
docker pull Jetson 官方镜像
使用下面命令 pull 所需镜像:
docker pull nvcr.io/nvidia/deepstream-l4t:6.2-triton
下载完成后,输入 docker image ls 即可显示当前系统中存在的镜像。
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
<none> <none> 60b9728b8120 8 days ago 0B
ubuntu 20.04 0341906bdafc 5 weeks ago 65.7MB
nvcr.io/nvidia/deepstream-l4t 6.3-samples a651320659bd 3 months ago 5.47GB
nvcr.io/nvidia/deepstream-l4t 6.2-triton d3cfcf05276d 9 months ago 14.9GB
nvcr.io/nvidia/l4t-tensorrt r8.4.1.5-devel 9d233de1abe7 14 months ago 10.2GB
nvcr.io/nvidia/l4t-base r35.1.0 dc07eb476a1d 15 months ago 713MB
接着可以使用 docker run 指令创建容器。以下是 NVIDIA 官方提供的创建容器指令。由于指令内容较长,建议自己写一个 .sh 脚本:
# 允许外部应用程序连接到主机的 X 显示器
xhost +
docker run -it --rm --net=host --runtime nvidia \
-e DISPLAY=$DISPLAY \
-w /opt/nvidia/deepstream/deepstream-6.2 \
-v /tmp/.X11-unix/:/tmp/.X11-unix \
nvcr.io/nvidia/deepstream-l4t:6.2-triton
先解释一下其中涉及到的指令:
- docker run:用于创建并启动一个容器。
- -it:组合指令,用于启动交互式(
-i)终端(-t)。 - --rm:退出容器后自动移除容器。注意这也意味着容器可写层会在退出后被删除。
- --net=host:让容器使用宿主机网络。
- --runtime=nvidia:指定 NVIDIA runtime。部分新版本也可以使用
--gpus,但 Jetson / L4T 环境中很多教程仍然使用--runtime=nvidia。 - -e:配置环境变量,这里指定容器内
DISPLAY变量为设备的$DISPLAY。 - -w:指定容器内工作目录,进入容器后会直接进入这个目录。
- -v:将宿主机目录与容器内目录形成映射。
在 Docker run 命令 中可以看到更完整的 docker run 命令。
环境的配置
官方镜像中已经包含了所需的大部分环境,包括 CUDA、cuDNN、TensorRT 等。但是当我在终端输入 ffmpeg 以及 opencv_version 时,显示当前镜像中并不存在这两个库,因此 OpenCV 和 FFmpeg 等相关库仍需自行安装。
首先更新软件包列表:
apt-get update
Docker 中默认是 root 权限,因此不需要 sudo 命令
可以使用 apt install 安装各种依赖库。
Docker 支持通过扩展现有镜像来创建新的镜像。实际上 Docker Hub 中很多镜像都是通过在 base 镜像中安装和配置所需软件构建出来的。
Docker 镜像的只读性
Docker 镜像是只读的,即镜像内容在创建后不可更改。这意味着镜像本身是不可变的,任何对容器的修改都会在容器运行时创建新的可写层(read-write layer)。重启同一个容器不会删除这个可写层;删除容器,或者使用 --rm 退出容器后,容器层数据才会丢失。Docker 提供了 volume 和 bind mount 这两种常用机制,用于在容器中持久保存数据。
当容器启动时,一个新的可写层 read-write layer 会加载到镜像顶部。这一层通常被称为容器层(container layer),容器层之下的都叫做镜像层。
为了保存以上变更,可以使用 docker commit。它允许将 Docker 容器保存为新的镜像,以便将来使用。
先列出所有正在运行的容器:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7ef30f88be87 nvcr.io/nvidia/l4t-base:r35.1.0 "/bin/bash" 22 hours ago Up 22 hours nice_panini
c623b1e3d293 nvcr.io/nvidia/l4t-base:r35.1.0 "/bin/bash" 22 hours ago Up 22 hours kind_darwin
8169d3923896 docker4orin:version0.1.1 "/bin/bash" 46 hours ago Up 46 hours pedantic_knuth
之后可以用 docker commit 命令对要保存更改的容器进行操作,语法如下:
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]
CONTAINER 为想要保存更改的容器;REPOSITORY[:TAG] 为希望设置的镜像名称和 tag。
OPTIONS 说明:
- -a:提交的镜像作者。
- -c:使用 Dockerfile 指令来创建镜像。
- -m:提交时的说明文字。
- -p:在 commit 时,将容器暂停。
比如我想要保存对 ID 为 8169d3923896 的容器的更改,就可以使用下面命令:
docker commit -a "funnywii" -m "update models" -p 8169d3923896 docker4orin:version0.1.2
之后可以使用 docker image ls 看到一个新的镜像生成:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker4orin version0.1.2 ea52e9b6de1c 22 hours ago 19.9GB
挂载宿主机的卷
方法是使用 docker run -v 进行本地目录挂载。在一次 docker run 命令中,-v 选项可以使用多次。
绑定目录是一种双向同步。你在宿主机上改变的每个文件都会在容器中改变,而容器中改变的每个文件也会在宿主机上改变。这种方法的优点是使用起来很直接,而且容易访问。
比如我想挂载宿主机硬盘中的程序,该程序位于 /media/nvidia/0739c4aa-df47-4f65-b632-6c50782120ab/CV/dockertest/Detection_dynamic(对不起硬盘名有点长)。在 docker run -v 命令中,: 前是宿主机路径,后是 Docker 容器内路径。
docker run -it --rm --net=host --runtime nvidia \
-e DISPLAY=$DISPLAY \
-w /opt/nvidia/deepstream \
-v /media/nvidia/0739c4aa-df47-4f65-b632-6c50782120ab/CV/dockertest/Detection_dynamic:/opt/nvidia/tempdir \
-v /tmp/.X11-unix/:/tmp/.X11-unix \
docker4orin:version0.1.2
运行后可以发现,Docker 内的 /opt/nvidia/tempdir 目录下存在和宿主机.../CV/dockertest/Detection_dynamic 相同的内容。而在宿主机或 Docker 内对挂载目录文件所做的更改,在另一侧也是同步的。
需要注意的是,既然是使用 Docker 内的环境运行代码,CMakeLists.txt 中相关库的路径也记得改成 Docker 中的路径。而且代码就不能在宿主机上进行编译了,仅能在 Docker 中编译。
Dockerfile 方法从 Jetson 官方基础镜像构建镜像
Dockerfile 是一种被 Docker 程序解释的脚本,它由一条条指令组成,每条指令对应 Linux 下面手动执行的一条命令。Docker 程序将这些 Dockerfile 指令翻译成真正的 Linux 命令。Dockerfile 有自己的书写格式和支持的命令,Docker 程序解决这些命令间的依赖关系,类似 Makefile。Docker 程序读取 Dockerfile 后,根据指令生成定制的 image。
相比 image 这种黑盒子,Dockerfile 这种显式脚本更容易被使用者接受。它明确表明 image 是怎么产生的。有了 Dockerfile,当需要定制额外需求时,只需在 Dockerfile 上添加或者修改指令,重新生成 image 即可,省去了反复手敲命令的麻烦。
相比上面的方法,Dockerfile 方法把手动要执行的一系列操作:下载镜像、运行镜像、下载、安装应用环境、运行应用等,按照 Dockerfile 格式写在脚本中。只要运行脚本,就可以完成本来要手动执行的多条命令。
Dockerfile 指令忽略大小写,但建议大写。使用 # 作为注释,每一行只支持一条指令,每条指令可以携带多个参数。Dockerfile 指令根据作用可以分为两种:构建指令和设置指令:
- 构建指令用于构建 image,其指定的操作不会在运行 image 的容器上执行。
- 设置指令用于设置 image 的属性,其指定的操作将在运行 image 的容器中生效。
Dockerfile 要注意的点
Dockerfile 中指令的用法,很多文章已经说得十分清楚,比如 Docker 入门系列(7)- Dockerfile 使用。
我就说一些自己当时的困惑和遇到的问题。
FROM本地镜像
Dockerfile 中的 FROM命令,可以基于本地或者网络镜像。如果你当时已经 pull 好了本地镜像,或者自行制作了基础镜像,可以通过docker image ls命令查看当前系统中存在的镜像,然后在FROM后面写本地镜像的[仓库:TAG] 即可。
WORKDIR
工作目录可能需要经常修改,后一个设置的 WORKDIR 会覆盖前一个。WORKDIR 后面的命令如果涉及到相对路径,会基于最新的一个 WORKDIR 进行。
需要注意,COPY 和 ADD 的源路径是相对于 build context 的;目标路径如果是相对路径,则相对于 WORKDIR。这能解释为什么有些 cd 指令会报错,也能解释为什么复制文件时经常找不到目标文件。
COPY和ADD指令
回顾基础知识中提到的上下文内容,COPY 和 ADD 命令不能拷贝 build context 之外的本地文件。因为执行 build 命令时,Docker 客户端会把上下文中的所有文件发送给 docker daemon。考虑 Docker 客户端和 Docker daemon 不在同一台机器上的情况,build 命令只能从上下文中获取文件。
NOTE:经过测试,使用 Dockerfile 构建镜像的方式在我这里似乎行不通。因此 jetson-ffmpeg 库的编译需要 NVIDIA runtime,而它通常只能在docker run 的时候通过--runtime=nvidia指定,或者修改 daemon.json 来设置 Docker 默认运行时。
如果你通过 Dockerfile 方式构建镜像,并直接在 Dockerfile 中编译 Jetson-FFmpeg,可能会遇到缺少 nvmpi 相关库的报错。这是因为标准docker build通常不能指定--runtime=nvidia:
$ docker build -t testbuild:v0.1.1 . --network=host --runtime=nvidia
unknown flag: --runtime
See 'docker buildx build --help'.
如果构建阶段必须访问 NVIDIA runtime 或 GPU,通常需要调整构建方式、提前准备好依赖、设置默认 runtime,或者避免在 build 阶段执行依赖 GPU runtime 的步骤。
参考文章
[1] NVIDIA Jetson Containers Tutorial
[3] Use These Jetson Docker Containers
[4] dusty-nv/jetson-containers DeepStream
[6] Docker 数据目录迁移
评论