使用Docker打包RMUA仓库以方便部署

考虑到目前笔者参与的项目常常出现设备损坏等问题,重新刷机并部署RMUA仓库需要花费大量时间,因此考虑使用Docker打包RMUA仓库以方便部署。本文笔者用一个周末时间,将从如何fork原始仓库到打包镜像再到托管到阿里云镜像服务的完整过程记录下来,尽可能保留所有细节以及延伸学习,以供自我学习与备忘。

写在前面:如何使用

如何快速部署该环境

快速部署该环境,可以直接拉取笔者打包好的镜像,执行以下命令:

1
docker pull ccr.ccs.tencentyun.com/dufolk/rmua:v1

拉取镜像后,首先最好先拉取 RMUA4Docker 仓库,对物理机的 USB 端口进行配置后再继续下面的操作。

1
2
3
git clone https://github.com/dufolk/RMUA4Docker.git
cd RMUA4Docker/roborts
./src/scripts/udev.sh create

最后需要创建一个脚本launch.sh,用于启动容器。launch.sh内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
# create the container
CONTAINER_NAME="roborts_ros"
echo "[INFO] Run the Container ${CONTAINER_NAME}"
sudo docker run \
-d \
--net=host \
--name ${CONTAINER_NAME} \
--privileged \
--volume /dev:/dev \
ccr.ccs.tencentyun.com/dufolk/rmua:v1 \
/bin/bash -c "tail -f /dev/null"
# check docker run result
if [ $? != 0 ]
then
echo "[WARN] Container already exists. Please wait for stopping and restarting the current container. "
sudo docker stop ${CONTAINER_NAME} >/dev/null 2>&1
sudo docker rm ${CONTAINER_NAME} >/dev/null 2>&1
sudo docker run \
-d \
--net=host \
--name ${CONTAINER_NAME} \
--privileged \
--volume /dev:/dev \
ccr.ccs.tencentyun.com/dufolk/rmua:v1 \
/bin/bash -c "tail -f /dev/null"
fi

该脚本用于创建并管理一个名为 “roborts_ros” 的 Docker 容器,主要功能包括:

  1. 挂载主机 /dev 目录到容器
  2. 自动检测并处理容器已存在的情况:
    • 停止并删除已存在的同名容器
    • 重新创建容器

通过执行sh ./launch.sh,我们已经在后台启动了容器,此时我们可以通过docker ps命令查看容器状态。

最后通过docker exec -it roborts_ros bash进入容器,即可使用RMUA仓库中的代码。下文是从零开始打包镜像的过程,供想要学习的同学参考。

从零开始构建仓库

创建Docker仓库

fork原始仓库

截至笔者写下文章的时间,RMUA 仓库在 Yue-0 的主页中,为构建完整的代码镜像,需要先 fork 仓库(防止原始仓库被 Yue-0 删除😂)。
完成 fork 后,笔者创建了 RMUA4Docker 仓库,用于打包 RMUA 仓库及其他依赖环境。这里可以很方便的使用 git 中的 submodule 管理子模块,目前我们只需要 Roborts-base 和 RMUA 这两个仓库作为子模块。

Git Submodule 是 Git 提供的子模块管理功能,允许将一个 Git 仓库作为另一个仓库的子目录嵌入。它保持父仓库与子模块的独立版本控制历史,解决以下场景:

  • 在项目中包含第三方库(如组件库、框架)
  • 维护多个有依赖关系的独立项目
  • 需要引用特定版本的外部代码

核心概念

  1. 父仓库(Superproject):包含子模块的主项目
  2. 子模块(Submodule):独立 Git 仓库,嵌入在父仓库中
  3. .gitmodules 文件:记录子模块路径、URL 和版本信息

常用命令

命令作用示例
添加子模块添加外部仓库作为子模块git submodule add <URL> <路径>
git submodule add https://github.com/Yue-0/RMUA.git src/RMUA
初始化子模块克隆父仓库后首次初始化子模块git submodule init
git submodule init -- <特定路径>
更新子模块获取最新子模块内容(默认签出提交记录版本)git submodule update
git submodule update --remote(获取最新提交)
递归操作处理嵌套子模块git clone --recurse-submodules <URL>
git submodule update --init --recursive
查看状态检查子模块状态git submodule status
git diff --submodule
更新引用修改指向的子模块提交进入子模块目录后:
git checkout main
git pull
返回父目录:
git add <子模块路径>
git commit -m "更新子模块引用"
删除子模块移除不再需要的子模块git rm --cached <路径>
删除 .gitmodules 中的对应条目
删除 .git/modules/<名称>

首先git clone https://github.com/dufolk/RMUA4Docker.git并进入仓库,然后添加子模块。

1
2
3
4
5
git submodule add https://github.com/dufolk/RMUA.git RMUA # 加入RMUA子模块
git submodule add https://github.com/RoboMaster/RoboRTS-Base.git roborts/src/ # 加入Robots-base子模块

git commit -m "first commit" # 提交代码
git push # 将代码推送到远程仓库

这样就完成了 RMUA4Docker 仓库的创建。现在我们尝试重新拉取 RMUA4Docker 仓库,会发现 RMUA 仓库及其他子模块下是空的。这里我们需要手动更新子模块。

1
git submodule update --init --recursive

即可成功拉取完整的 RMUA4Docker 仓库。

这里有一些文件(如client.py)并非Yue-0维护,所以我单独上传了。

打包镜像

我们借鉴 Roborts-base 仓库中的 Dockerfile 文件,将其修改为 RMUA4Docker 仓库的 Dockerfile 文件。

1
touch Dockerfile build_image.sh

build_image.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" # 获取当前脚本所在目录
IMAGE_REPO="roborts_ros" # 镜像仓库名称
ARCH_TYPE="$(arch)" # 架构类型
ROS_VERSION="noetic" # ROS版本
if [ $ARCH_TYPE == "x86_64" -o ${ARCH_TYPE} == "aarch64" ] # 判断架构类型
then
IMAGE_TAG="roborts_base_${ARCH_TYPE}" # 镜像标签
if [ $ARCH_TYPE == "x86_64" ] # 根据架构类型选择基础镜像
then
BASE_IMAGE="ubuntu:20.04"
else
BASE_IMAGE="nvcr.io/nvidia/l4t-jetpack:r35.4.1"
fi
else
# echo "ARCH_TYPE $ARCH_TYPE is supported. Valid architecture is [aarch64, x86_64]"
# exit 1
# 我们车上的架构是aarch64,所以这里直接默认使用aarch64
IMAGE_TAG="roborts_base_aarch64"
BASE_IMAGE="nvcr.io/nvidia/l4t-jetpack:r35.4.1"
fi

cd ${DIR}

# 构建镜像
# --build-arg BASE_IMAGE=${BASE_IMAGE} 基础镜像
# -t ${IMAGE_REPO}:${IMAGE_TAG} 镜像名称
# -f Dockerfile 选择Dockerfile
# . 当前目录
sudo docker build \
--build-arg BASE_IMAGE=${BASE_IMAGE} \
-t ${IMAGE_REPO}:${IMAGE_TAG} \
-f Dockerfile .

在这里,我们将所有源代码都通过 Dockerfile 拷贝到镜像中,包括 RMUA 仓库及 Robots-base 仓库,从而避免在运行时再拉取和部署代码。Dockerfile 是打包镜像的关键文件,它包含了所有打包镜像所需的指令和说明。通过分享 Dockerfile,我们可以轻松的将环境在任何地方打包。打包好镜像后,也可以在任何地方部署镜像。
我们也可以不把所有代码都打包到镜像中,而是在运行时通过挂载本地目录部署代码(如原 roborts 中 Dockerfile 的操作),这样可以减少镜像的体积,同时更好的适应代码的变化。但考虑到我们目前的初衷是快速部署并运行,并不对代码进行修改,因此我们将所有代码都打包到镜像中。如果想在容器中进行开发,也可以考虑在 vscode 中安装相应的 docker 插件,直接在容器中进行开发。

nvidia 官方 docker 与 jetpack 版本对应关系

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 基础镜像
ARG BASE_IMAGE=nvcr.io/nvidia/l4t-jetpack:r35.4.1
FROM ${BASE_IMAGE}

# 安装ROS
RUN apt update \
&& apt install wget python3-yaml -y \
# 安装noetic
&& echo "chooses:\n" > fish_install.yaml \
&& echo "- {choose: 1, desc: '一键安装:ROS(支持ROS和ROS2,树莓派Jetson)'}\n" >> fish_install.yaml \
&& echo "- {choose: 1, desc: 更换源继续安装}\n" >> fish_install.yaml \
&& echo "- {choose: 2, desc: 清理三方源}\n" >> fish_install.yaml \
&& echo "- {choose: 3, desc: noetic(ROS1)}\n" >> fish_install.yaml \
&& echo "- {choose: 1, desc: noetic(ROS1)桌面版}\n" >> fish_install.yaml \
&& wget http://fishros.com/install -O fishros && /bin/bash fishros \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& apt-get clean && apt autoclean

# 安装ROS依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
ros-noetic-tf ros-noetic-nav-msgs ros-noetic-geometry-msgs ros-noetic-rplidar-ros libgoogle-glog-dev python3-pip \
libopencv-dev python3-opencv \
&& rm -rf /var/lib/apt/lists/*

# 拷贝roborts工作空间和RMUA工作空间
COPY ./RMUA /root/RMUA
COPY ./roborts /root/roborts
COPY ./client.py /root/RMUA/src/sentry/scripts/client.py
COPY ./onnxruntime_gpu-1.18.0-cp38-cp38-linux_aarch64.whl /root/onnxruntime_gpu-1.18.0-cp38-cp38-linux_aarch64.whl
COPY ./libstdc++.so.6.0.29 /lib/aarch64-linux-gnu/libstdc++.so.6.0.29

# 编译roborts和RMUA
WORKDIR /root
RUN /bin/bash -c "source /opt/ros/noetic/setup.bash && \
cd roborts && catkin_make && \
cd ../RMUA/src && \
source /root/roborts/devel/setup.bash && \
rm -f /root/RMUA/src/CMakeLists.txt && \
catkin_init_workspace && \
cd vision && \
pip install -r requirements.txt && \
pip install /root/onnxruntime_gpu-1.18.0-cp38-cp38-linux_aarch64.whl && \
cd ../.. && \
catkin_make --only-pkg-with-deps sentry && \
catkin_make -DCATKIN_WHITELIST_PACKAGES=\"decision;navigation;vision\" && \
rm /root/onnxruntime_gpu-1.18.0-cp38-cp38-linux_aarch64.whl"
RUN echo "source /root/RMUA/devel/setup.bash" >> /root/.bashrc

RUN rm /lib/aarch64-linux-gnu/libstdc++.so.6 \
&& ln -s /lib/aarch64-linux-gnu/libstdc++.so.6.0.29 /lib/aarch64-linux-gnu/libstdc++.so.6

CMD ["bash"]
  • 基础镜像:使用 NVIDIA Jetpack 的 L4T 版本作为基础镜像。
  • 安装 ROS:通过 fishros 脚本[1]安装 ROS Noetic 版本,并安装相关依赖。
  • Onnxruntime-gpu:Onnxruntime-gpu 是一个用于运行 ONNX 模型的高性能推理引擎,支持 GPU 加速。这里我们安装了适用于 ARM64 架构的版本。但是Onnxruntime-gpu在 Jetson 上有严格的版本匹配要求,因此需要确保安装的版本与 Jetpack 版本兼容。
  • libstdc++.so.6:Nvidia 官方给出的 Jetson 镜像上的 libstdc++.so.6 版本较低,会导致Onnxruntime-gpu无法运行,因此需要手动替换为新版本。这个问题在官方论坛上有人指出过,但是官方表示“知道了,但我们不修,你去装新版 Jetson 镜像吧”,也是挺抽象的。我们只好拷贝一个新版本的 libstdc++.so.6 到镜像中,然后软链接到 /lib/aarch64-linux-gnu/libstdc++.so.6。(这个新版本的 libstdc++.so.6 可以从 anaconda 安装路径找到,虽然这里没有安装 anaconda,但我从其他地方拷贝来了一个)

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。通过定义一系列命令和参数,Dockerfile 指导 Docker 构建一个自定义的镜像。

FROM 和 RUN 指令的作用

FROM:定制的镜像都是基于 FROM 的镜像。
RUN:用于执行后面跟着的命令行命令。有以下俩种格式:
shell 格式:

1
2
RUN <命令行命令>
# <命令行命令> 等同于,在终端操作的 shell 命令。

exec 格式:

1
2
3
RUN ["可执行文件", "参数1", "参数2"]
# 例如:
# RUN ["./test.php", "dev", "offline"] 等价于 RUN ./test.php dev offline

注意:Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。例如:

1
2
3
4
FROM centos
RUN yum -y install wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN tar -xvf redis.tar.gz

以上执行会创建 3 层镜像。可简化为以下格式:

1
2
3
4
FROM centos
RUN yum -y install wget \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& tar -xvf redis.tar.gz

如上,以 && 符号连接命令,这样执行后,只会创建 1 层镜像。

指令

Dockerfile 指令说明
FROM指定基础镜像,用于后续的指令构建。
MAINTAINER指定 Dockerfile 的作者/维护者。(已弃用,推荐使用 LABEL 指令)
LABEL添加镜像的元数据,使用键值对的形式。
RUN在构建过程中在镜像中执行命令。
CMD指定容器创建时的默认命令。(可以被覆盖)
ENTRYPOINT设置容器创建时的主要命令。(不可被覆盖)
EXPOSE声明容器运行时监听的特定网络端口。
ENV在容器内部设置环境变量。
ADD将文件、目录或远程 URL 复制到镜像中。
COPY将文件或目录复制到镜像中。
VOLUME为容器创建挂载点或声明卷。
WORKDIR设置后续指令的工作目录。
USER指定后续指令的用户上下文。
ARG定义在构建过程中传递给构建器的变量,可使用 docker build 命令设置。
ONBUILD当该镜像被用作另一个构建过程的基础时,添加触发器。
STOPSIGNAL设置发送给容器以退出的系统调用信号。
HEALTHCHECK定义周期性检查容器健康状态的命令。
SHELL覆盖 Docker 中默认的 shell,用于 RUNCMDENTRYPOINT 指令。

使用 QEMU 模拟 arm64 环境(如果在amd64机器上测试镜像)

由于 jetpack 是 arm64 系统架构,而我们的宿主机器一般为 amd64,因此需要使用 QEMU 模拟 arm64 环境,才可以正常编译和使用 jetpack 镜像环境。

1
sudo apt-get install qemu-user-static

构建镜像

在RMUA4Docker仓库中,执行以下命令构建镜像:

1
2
chmod +x build_image.sh # 赋予脚本可执行权限
./build_image.sh # 执行脚本构建镜像

此处可能会遇到网络问题,导致无法下载依赖包。可以尝试更换源、用国内镜像加速服务或使用代理。我这边用了国内的付费加速服务轩辕镜像加速服务。

拉取镜像

目前笔者已将镜像打包并托管到腾讯云镜像服务中,这个步骤在腾讯云提供的文档中已经介绍的非常详细,不再赘述。我们可以直接拉取镜像(见本文开头)。

结语

通过本文的记录,笔者成功地将 RMUA 仓库打包成 Docker 镜像,并托管到腾讯云镜像服务中。这样一来,可以在任何地方快速部署 RMUA 仓库,避免了设备损坏或其他问题导致的时间浪费。
希望本文对其他同学也有所帮助,能够在使用RMUA仓库时更加方便快捷。

参考资料


使用Docker打包RMUA仓库以方便部署
http://dufolk.github.io/2025/06/28/RMUA_docker/
作者
Dufolk
发布于
2025年6月28日
许可协议