关于Docker的基础知识见:FunnyWii's Zone Docker基础知识
- 硬件设备:天准Orin,基于Nvidia Jetson Orin
- 系统版本:Ubuntu 20.04
- 需打包的库:CUDA,OPENCV
值得一提的是,我的设备是ARM架构的。所以要注意一点,使用 Docker 构建镜像时, CPU 指令集不同,尽管可以在 X86 设备上用 docker pull --platform=linux/arm64
拉取用于 ARM 设备的镜像,但无法使用 docker run
或 docker build
运行或是通过构建的方法修改该镜像。
修改docker pull
的默认路径
老版的Orin平台,虽然挂载了一个512GB的SSD硬盘,但是其根目录所在的硬盘只有64GB,对于动辄几个GB甚至几十个GB的镜像来说还是不够的,毕竟自己乱七八糟的库都占了十来个G。
因此在pull
镜像之前,需要修改docker pulll
的默认下载路径。默认路径为/var/lib/docker/
可以使用docker info | grep “Root Dir”
指令查看当前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/
,但是由于软链的原因,实际上是在向新的存储目录中写入。
commit
方法修改并保存Jetson官方镜像
Jetson官方镜像的选择
在 dusty-nv/jetson-containers: Machine Learning Containers for NVIDIA Jetson and JetPack-L4T (github.com) 或 Cloud-Native on Jetson | NVIDIA Developer 都可以找到 Jetson 官方 Docker 镜像。由于我需要使用视觉和CUDA相关的环境,所以我选择了 deepstream
的 Docker。注意镜像版本和L4T&Jetpack的对应关系。 我的Jetpack是5.0.2GA,对应L4T为35.1.0,不过我直接下载了deepstream的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 images 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 +
# 允许外部应用程序连接到主机的X显示器:
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
表示共用 Jetson 设备网络。
--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
命令,而且其根本没有安装sudo
命令,若要使用也需要自行安装。
之后便可以使用apt-get install
安装git
,sudo
以及各种依赖库。
Docker支持通过扩展现有镜像来创建新的镜像,实际上Docker Hub中99%的镜像都是通过在Base镜像中安装和配置需要的软件构建出来的。
Docker镜像的只读性
Docker 镜像是只读的,即镜像的内容在创建后不可更改。这意味着镜像本身是不可变的,任何对容器的修改都会在容器运行时创建新的可写层(Read-Write layer)。这意味着重新启动一个容器会删除你在容器中存储的所有数据,而且对容器的操作均不会影响到镜像。但是Docker提供了卷和绑定挂载,这是两种在Docker容器中持久保存数据的机制。
当容器启动时,一个新的可写层 Read-Write layer 被加载到镜像的顶部,这一层通常被称为容器层 Container,容器层之下的都叫做镜像层。
为了保存以上变更,需要使用docker commit
,它允许将Docker容器保存为新的镜像,以便将来使用。
先使用 docker ps
列出所有正在运行的容器:
$ 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时,将容器暂停。
其中-a
,-m
的首末最好加" "
。
比如我想要保存对ID为8169d3923896的容器的更改,就可以使用下面命令
docker commit -a "funnywii" -m "update models" -p 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或宿主机上是同步的。
需要注意的是,既然是使用Docker内的环境运行代码,CMakelists中相关库的路径也记得改成Docker中的路径。而且代码就不能在宿主机上进行编译了,仅能在Docker中编译。
Dockerfile
方法从Jetson官方基础镜像构建镜像
Dockfile
是一种被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
的相对路径就是相对于 WORKDIR 指定的路径。
这个能解释为什么cd
指令有时会报错,与当前的WORKDIR
相关。
COPY
和ADD
指令
回顾基础知识中提及到的上下文内容, COPY
和 ADD
命令不能拷贝上下文之外的本地文件。 因为在执行 build
命令时,Docker 客户端会把上下文中的所有文件发送给 docker daemon
。 考虑 Docker 客户端和 Docker daemon 不在同一台机器上的情况,build
命令只能从上下文中获取文件。
NOTE:经过测试,使用Dockerfile
的方式构建镜像的方式在我这里似乎行不通。因此 jetson-ffmpeg
库的编译,需要Nvidia Runtime,而它只能在 docker run
的时候通过--runtime=nvidia
进行指定,或者修改daemon.json
来设置Docker的默认运行时。
如果你通过Dockerfile
的方式构建镜像并直接在Dockerfile
中git clone
Jetson-FFMPEG库并编译,则会遇到缺少nvmpi
相关库的报错,这是因为docker build
并不能指定runtime
:
$docker build -t testbuild:v0.1.1 . --network=host --runtime=nvidia
unknown flag: --runtime
See 'docker buildx build --help'.
参考文章
[1] https://developer.nvidia.com/embedded/learn/tutorials/jetson-container#h.643lq522tv1d
[2] https://leonis.cc/sui-sui-nian/2023-07-28-build-arm-docker-image-on-x86.html
[3] https://jetsonhacks.com/2023/09/04/use-these-jetson-docker-containers-tutorial/
[4] https://github.com/dusty-nv/jetson-containers/tree/master/packages/deepstream
[5] http://nvidia.zhidx.com/index.php?m=content&c=index&a=show&catid=6&id=2768
[6] https://zhuanlan.zhihu.com/p/549143820
[7] https://docs.nvidia.com/metropolis/deepstream/dev-guide/text/DS_Quickstart.html#id5