基于 Jetson 官方 Docker 镜像制作所需镜像的步骤

FunnyWii
FunnyWii
发布于 2023-11-02 / 34 阅读
0
0

基于 Jetson 官方 Docker 镜像制作所需镜像的步骤

关于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 rundocker 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 为例。

Jetson-DeepStream.png

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安装gitsudo以及各种依赖库。

Docker支持通过扩展现有镜像来创建新的镜像,实际上Docker Hub中99%的镜像都是通过在Base镜像中安装和配置需要的软件构建出来的。

Docker镜像的只读性

Docker 镜像是只读的,即镜像的内容在创建后不可更改。这意味着镜像本身是不可变的,任何对容器的修改都会在容器运行时创建新的可写层(Read-Write layer)。这意味着重新启动一个容器会删除你在容器中存储的所有数据,而且对容器的操作均不会影响到镜像。但是Docker提供了卷和绑定挂载,这是两种在Docker容器中持久保存数据的机制。

当容器启动时,一个新的可写层 Read-Write layer 被加载到镜像的顶部,这一层通常被称为容器层 Container,容器层之下的都叫做镜像层。

镜像层.png

为了保存以上变更,需要使用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 使用

我就说一些自己当时的困惑和遇到的问题。

  1. FROM本地镜像

Dockerfile中的FROM命令,可以基于本地或者网络镜像。如果你当时已经pull好了本地镜像,或者自行制作了基础镜像。那可以通过docker image ls命令查看当前系统中存在的镜像。然后FROM本地镜像的 [仓库:TAG] 即可.

  1. WORKDIR

工作目录可以需要经常修改,后一个设置的WORKDIR会覆盖前一个。WORKDIR后面的命令如果涉及到路径,会基于最新的一个WORKDIR进行。COPYADD的相对路径就是相对于 WORKDIR 指定的路径。

这个能解释为什么cd指令有时会报错,与当前的WORKDIR相关。

  1. COPYADD指令

回顾基础知识中提及到的上下文内容, COPY ADD 命令不能拷贝上下文之外的本地文件。 因为在执行 build 命令时,Docker 客户端会把上下文中的所有文件发送给 docker daemon。 考虑 Docker 客户端和 Docker daemon 不在同一台机器上的情况,build 命令只能从上下文中获取文件。

NOTE:经过测试,使用Dockerfile的方式构建镜像的方式在我这里似乎行不通。因此 jetson-ffmpeg库的编译,需要Nvidia Runtime,而它只能在 docker run的时候通过--runtime=nvidia进行指定,或者修改daemon.json 来设置Docker的默认运行时。
如果你通过Dockerfile的方式构建镜像并直接在Dockerfilegit 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


评论