Docker 开发:从零到英雄

概述

本教程是从您第一次使用 Docker 到可用于使用 Docker 开发 ROS 2 应用程序的命令和流程的实践演练。 无论如何,这并不是详尽的介绍,但应该可以帮助您从无到有,成为可以日常用于开发和测试的功能流程。 如果您已经了解 Docker 的一些基础知识,则可以跳到后面有关开发和部署的部分。 您还可以在本教程的附录中找到一组 Docker 映像,这些映像对于使用 Nav2 或其容器化部署进行开发非常有用。 相同的流程模板也可用于其他公司应用程序和项目。

其他一些有用的资源:

预赛

Docker 是一种用于在隔离环境(称为“容器”)中构建、部署、测试和使用软件的工具。 这与 VM 的不同之处在于它与主机操作系统共享相同的 Linux 内核,从而可以更快地启动和共享主机资源。 通过在此隔离环境中构建或部署软件,您可以确保许多用户、机器人或服务器在多个实例中运行具有相同软件版本的相同软件。 它为您提供了一个受控的工作环境,可以在其他开发人员的计算机上重现,甚至可以在与您的计算机当前运行的操作系统不同的(基于 Linux 的)操作系统中工作。 例如,您可以在 Nvidia Jetson 的 Jetpack 5.1(这是 20.04 的版本)上运行包含 ROS 2 Humble 的 22.04 Docker 容器,并将该容器部署到机器人队列。

在常见的 Docker 语言中,“镜像”是内置的“Dockerfile”,可用于创建“容器”。 因此,*容器*是 docker *镜像*的独立的、可运行的实例。 “Dockerfile” 是一组关于如何构建映像以创建某种工作环境的指令,并且通常包含要在该环境中部署的应用程序。 Dockerfile 指令集有许多选项,例如:

  • ARG: Obtain build-time arguments

  • FROM: Specify a base image to build from

  • RUN: Run a particular command

  • WORKDIR: Set the working directory

  • COPY: Copy a file or directory

  • ENV: Set an environmental variable

其中大部分都是不言自明的,但您可以参考 Docker 文档来了解更多信息并查看全套内容。

值得强调的两个特殊命令是“CMD”和“ENTRYPOINT”,您将在许多“Dockerfile”的底部看到它们。

  • ENTRYPOINT: A command to execute when the container is spun up which cannot be overridden

  • CMD: A command to execute when a container is spun up which can be overridden

在 ROS Docker 容器的上下文中,您将看到它们创建一个 bash 会话并执行一个 ros_entrypoint.sh 脚本。 该脚本只是为您的发行版获取 ROS 环境``/opt/ros/…/setup.bash``,因此当您打开容器时,您就可以开始了。 但它们可用于执行更高级的操作,例如运行应用程序或触发其他事件。

重要的 Docker 命令

同样,在我们继续之前,讨论一些 docker 命令行命令也很重要,但并不详尽。 还有许多其他命令,但这些是我们将在本演练中使用的基本命令,并且可能是您日常使用的最常见命令。 在本教程中,为每个选项构建一些重要的选项标志,但现在让我们讨论基础知识:

  • docker run: Runs a given docker image to create a container

  • docker build: Builds a Dockerfile to create an image

  • docker pull / push: Pulls a docker image from another location or pushes a built image to another location

  • docker stop / kill: Stops or kills a running docker container

  • docker ps: Lists a set of docker images that are currently running

  • docker attach: Attach a terminal to a background running docker container

  • docker exec: Execute a command in a provided container

  • docker images: Lists a set of containers pulled or built on your computer

探索你的第一个容器

让我们从获取最新、最强大的 ROS 2 Rolling 开始本教程。 使用 OSRF DockerHub 服务器,我们可以下载许多不同的 ROS 2 Docker 镜像来使用,而无需创建 Dockerfile 或自己构建它们。

sudo docker pull osrf/ros:rolling-desktop-full

然后您应该看到以下内容,其中图像被下拉到多个层,并最终在完成后返回终端。

steve@reese:~$ sudo docker pull osrf/ros:rolling-desktop-full
rolling-desktop-full: Pulling from osrf/ros
31bd5f451a84: Already exists
d36cae3fb404: Already exists
8d68f36a56a7: Already exists
299f725c4bf1: Already exists
6e16227afc48: Already exists
02457a85146c: Downloading   83.7MB/106.5MB
fe0cbdee2808: Download complete
4b4dbddf506a: Downloading  92.86MB/98.14MB
0da90b52c355: Download complete
64de492566b2: Download complete
167d95ac0fce: Download complete
e727072615d0: Downloading  82.61MB/809.8MB
d15e176ed0af: Waiting

如果您随后尝试将此映像作为容器(映像的实例)运行:

sudo docker run osrf/ros:rolling-desktop-full

您应该看到它运行一秒钟然后退出终端。耶!有用!但是……这不太有用,现在是吗? 我们的 ROS 2 Docker 镜像的“ENTRYPOINT”仅来源 ROS 2 安装,因此程序返回已完成。 如果我们想进入容器并在该环境中做一些对我们自己有用的事情,我们需要打开与容器的交互式终端会话。 使用“-it”标志很容易做到这一点:

sudo docker run -it osrf/ros:rolling-desktop-full

您现在应该看到一个终端会话打开,命令提示符为“root@<some hash>:/#”。 这是你的 Docker 容器。 环顾四周,它应该看起来像任何其他 Linux 操作系统。 如果你进入``/opt/ros/rolling``,你应该看起来很熟悉!


如果您打开一个新终端并运行“sudo docker ps”,您现在应该会看到一个容器实例在您的系统上运行。 该容器的 ID 应与命令提示符中的哈希值匹配。 我们之前提到过,旋转时的容器将自动获取 ROS 安装,因此我们应该能够立即进行操作:

echo $ROS_DISTRO  # --> rolling
ros2 run demo_nodes_cpp talker # --> [INFO] [1707513434.798456374] [talker]: Publishing: 'Hello World: 1'
touch navigator_dockerlayer.txt
l # <-- you should see this file

好的!这一切都有效。现在,如果我们退出交互式会话(输入“exit”),我们应该重新进入我们的计算机。 在第二个终端中,如果重新运行“sudo docker ps”,您应该看到容器列表现在为空,因为我们的容器不再运行。 如果您想查看容器的完整列表,包括退出的容器,您可以使用标志“-a”来显示所有容器。

steve@reese:~$ sudo docker ps -a
CONTAINER ID   IMAGE                           COMMAND                  CREATED         STATUS                          PORTS     NAMES
7ec0e0b7487f   osrf/ros:rolling-desktop-full   "/ros_entrypoint.sh …"   5 minutes ago   Exited (0) About a minute ago             strange_tesla
9ccd97ac14f9   osrf/ros:rolling-desktop-full   "/ros_entrypoint.sh …"   7 minutes ago   Exited (0) 7 minutes ago                  zen_perlman

可以看到我们的容器成功退出了。如果我们现在再次运行我们的 docker 镜像,您应该会看到它重新列出,而没有“-a”。

sudo docker run -it osrf/ros:rolling-desktop-full

当我们在这里时,让我们的容器“ls”。哦不!我们的“navigator_dockerlayer.txt”文件丢失了! 这完全是可以预料到的。当我们退出容器时,该图像的实例将被销毁 - 再也不会被看到。 当我们再次运行该图像时,我们将生成一个全新的、干净的图像实例。 没有什么是持续存在的。这是理解前进的重要行为。 对于开发来说,这是因为按错按钮而失去一天工作的噩梦。 对于部署来说,这是一件幸事,因为您可以干净地重新启动,而不会产生先前失败的会话中的任何工件,并从干净的状态开始。 我们将在本教程后面讨论如何在会话之间保留数据,所以不用担心!


在我们的新集装箱仍处于开放状态的情况下,让我们探索如何跨多个码头使用一个集装箱。如果您要在两个终端中运行“docker run”命令,则需要使两个单独的容器彼此隔离。 相反,我们需要在容器中打开一个新会话。查看终端的哈希值或 sudo docker ps 来查找其 ID,使用 exec 命令在容器中执行命令 bash 。

sudo docker exec -it bce2ad161bf7 bash  # <-- use your ID

这将打开一个到容器的新的交互式会话,并且“exec”-utes命令“bash”为我们提供一个可以使用的shell(Dockerfile中的“CMD”为我们的启动终端执行此操作) )。 由于这不是新启动的容器,因此“ENTRYPOINT”脚本未运行。如果您尝试再次运行 talker 演示,它将找不到“ros2”命令。 不用担心,只需获取“/opt/ros/rolling/setup.bash”安装即可。

在容器中的任一终端会话中,如果您创建一个新文件,您应该能够在另一个终端会话中看到它,因为这是同一个容器!

touch navigator_alligator.txt
ls # <-- see the new file
# move to the other terminal
ls # <-- also see new file

现在,当我们打开同一个 docker 容器的两个终端时,我们可以做一些有趣的事情。让我们运行经典的谈话者/听众演示。在两个终端中的每一个中,运行以下命令之一。

ros2 run ros2 run demo_nodes_cpp talker
ros2 run demo_nodes_py listener

如果您现在打开计算机的第三个终端并运行“ros2 topic list”,您会发现明显缺乏主题。

steve@reese:~$ ros2 topic list
/parameter_events
/rosout

什么给?该容器与您的主机系统隔离,因此容器中发生的任何事情当前对您的主计算机都是不可用的。 让我们退出两个容器终端实例(“exit”)并讨论更多需要了解的“docker run”标志。 这次,我们希望将 ROS 暴露给我们更广泛的系统,包括我们的主机。这次,我们将使用标志“–net=host”,这将网络设置为看起来像主机系统(即您的计算机)。

sudo docker run -it --net=host osrf/ros:rolling-desktop-full

在本次会话中,如果我们运行 talker ros2 run demo_nodes_py talker,现在我们应该能够从我们的主机订阅它!

steve@reese:~$ ros2 topic echo /chatter
data: 'Hello World: 0'
---
data: 'Hello World: 1'
---
data: 'Hello World: 2'
---

让我们讨论一下如何使容器的运行时间比交互式终端会话的运行时间更长。 您希望容器比您寿命更长或在后台运行的原因有很多,因此这就是“-d”标志的用途,或者说是分离的。 让我们首先显示没有使用“sudo docker ps”运行的容器。接下来启动一个带有该标志的新容器。

sudo docker run -it --net=host -d osrf/ros:rolling-desktop-full

您将看到该命令运行一会儿然后返回。 sudo docker ps 现在应该显示容器正在运行。 复制该容器 ID,我们现在可以“附加”到它:

sudo docker attach e1d7e035a824  # <-- use your ID

您现在应该处于终端会话中。完成工作后,如果您想停止容器,可以像我们在本教程中所做的那样退出(“exit”),这也将停止容器。 如果您希望让容器保持运行状态,可以使用按键序列 Control+P+Q 退出,但让容器保持运行状态。 无论哪种情况,您都可以使用“ps”向自己展示这一点。 如果您让它继续运行,现在希望从外部停止它,您可以执行以下操作。退出可能需要一些时间。

sudo docker stop e1d7e035a824  # <-- use your ID

最后,“docker images”是一个命令,用于告诉您已经构建或提取了哪些可供使用的 docker 镜像。该列表会随着时间的推移而扩展,并且是了解您需要使用的内容的有用资源。

steve@reese:~$ sudo docker images
REPOSITORY   TAG                    IMAGE ID       CREATED        SIZE
osrf/ros     rolling-desktop-full   7cd0c5068235   6 days ago     3.86GB

Note

如果出现“无法创建共享内存管理器”或类似错误,请使用“–shm-size=100mb”命令增加容器中的共享内存缓冲区大小。

了解 ROS Docker 镜像

现在我们已经了解了一些 Docker 的基本功能并探索了 Rolling Desktop Full 容器,接下来让我们更详细地了解一下必须在 ROS 中使用的 Docker 镜像。 OSRF 托管一个 DockerHub 服务器,其中包含您可以提取和使用的所有 ROS 发行版的映像。 对于每个发行版,都有几个变体:

  • ros-core: Contains only the ROS core communication protocols and utilities

  • ros-base: Contains ros-core and other core utilities like pluginlib, bond, actions, etc

  • perception: Contains ros-base and image common, pipeline, laser filters, laser geometry, vision opencv, etc

  • desktop: Contains ros-base and tutorials, lifecycle, rviz2, teleop, and rqt

  • desktop-full: Contains desktop, perception and simulation

这些与使用“apt install ros-rolling-desktop-full”相同,但采用容器形式。 这些容器中的每一个都使用“FROM”构建前一个容器,然后安装所描述的二进制文件以服务于容器用户。 您使用哪个取决于您的应用程序和需求,但 osrf/ros:<distro>-ros-base 是开发和部署的良好默认值。 我们在本教程中使用的是桌面型,以便于使用 rviz2 和此类内置电池。

您可以像以前一样拉取和使用它们,例如:

sudo docker pull ros:rolling-ros-base
sudo docker pull osrf/ros:humble-desktop

请注意,某些容器可能需要“osrf/”,而其他容器可能不需要。 osrf/ 镜像由 osrf 发布,而无前缀的镜像是官方 docker 库的一部分。 一般来说,桌面安装带有``osrf/````,而ros core和base则没有。

对于基于 Docker 的开发

如前所述,如果我们在 Docker 容器中创建和修改文件,这些文件在容器退出后不会保留。 如果我们想做一些在镜像之间持续存在的开发工作,明智的做法是在运行时将一个“卷”安装到 Docker 容器上。 这只是一个花哨的谈话,用于将主机公司的一组给定目录链接到容器,以便可以在容器内读取、修改和删除它们并反映在外部。 这样,即使您关闭本地文件系统中的容器,您的工作也将持续存在,就好像它是在不使用容器的情况下开发的一样。 它的一个很棒的功能是,您实际上可以在一个容器中构建工作区,销毁该容器,然后继续开发并稍后在新的容器实例中重建,前提是(1)两次使用相同的映像,并且(2)容器内的安装位置每次都是相同的。

我们使用“-v”标志(用于卷)来完成此操作。还有其他选项可以做到这一点,但这是最直接的。 它采用“-v what/local/dir:/absolute/path/in/container”形式的参数。 如果我们在工作区的根目录中启动一个容器,则以下命令将启动 docker 容器,共享主机的网络,并将工作区 (.) 放入目录``/my_ws_docker`` 下的容器中:

sudo docker run -it --net=host -v .:/my_ws_docker  osrf/ros:rolling-desktop-full

ls
cd my_ws_docker
touch navigator_activator.txt

如果您转到另一个终端中的工作区,您现在应该会看到该文件反映在您的计算机上!如果我们运行 rosdep 在 docker 容器中安装依赖项,我们现在应该能够构建您的工作区。

apt update
rosdep init
rosdep update
rosdep install -r -y --from-paths . --ignore-src
colcon build

现在,您可以使用 VSCode 或您喜欢的代码编辑器对代码进行任何更改,并将其反映在容器中以进行构建和测试! 如果您使用多个 ROS 发行版或主机操作系统本身不支持的 ROS 发行版(例如 Nvidia Jetsons 上的 Jetpack 5.1 上的 Humble),此功能尤其强大。 然而,随着时间的推移,当您启动新容器时,必须等待所有依赖项手动安装,这确实会变得烦人。 因此,在提供的 ROS Docker 映像之一上进行构建以创建您自己的自定义开发映像非常有用,其中包含构建应用程序所需的包和环境。 这样,您就可以简单地跳入容器并立即开始构建。

树立发展形象

构建新容器很容易。 Docker 镜像的组织指令在“Dockerfile”中概述。 通常,它们以导入“FROM”开始,以设置要构建的起始容器。在我们的例子中,是 ROS 2 滚动图像。 然后,我们运行一系列“RUN”命令来执行设置依赖项的操作,以便在启动容器时让它们可供使用。 在“附录”中,您将找到一个可用于在 Nav2 上进行开发的示例开发映像。它从滚动“ros-base”开始,下载 Nav2,并在其软件包上运行 rosdep 以安装所有依赖项。 这些步骤完成后,图像就已为任何后续 Nav2 构建做好了准备。

您可以使用“docker build”构建此映像

sudo docker build -t nav2deps:rolling .

其中“-t”设置容器的标记名称以供以后使用。 需要注意的是,即使您的安装和构建空间将反映在主机工作区中,但在 docker 容器内编译时它们无法在本地运行。 此示例开发映像还升级了软件包,这打破了系统和“ros-base”安装软件包的严格版本控制。 对于部署情况,您希望确保所有软件包的版本相同 - 但是对于 ROS 2 Rolling,由于实时开发,ABI 和 API 不承诺稳定, 升级很有用,这样您的源代码就可以针对最新和最好的进行构建。

来自 Docker 的可视化

一些跳过这一点的人可能会注意到,当启动涉及 GUI(RQT、Rviz2、Gazebo)的应用程序时,它会崩溃并且永远不会出现。 Docker 的隔离不仅适用于网络,还适用于可视化和其他资产。 因此,我们必须专门启用 GUI 的分割,使其出现在我们的屏幕上。

  • --privileged: Bypasses many of the checks to field the container from the host system. A hammer smashing isolation.

  • --env="DISPLAY=$DISPLAY: Sets display to use for GUI

  • --volume="${XAUTHORITY}:/root/.Xauthority": Gets important info from the XServer for graphics display

总的来说,您现在应该能够在 docker 容器内打开 rviz2!

sudo docker run -it --net=host --privileged \
    --env="DISPLAY=$DISPLAY" \
    --volume="${XAUTHORITY}:/root/.Xauthority" \
    osrf/ros:rolling-desktop-full
rviz2

此时,如果仍有错误,请检查文档以了解要使用的正确标志。 (即使您复制+粘贴,也不用花超过 10 分钟就能找到有效的组合。) 如果您使用的是 Nvidia Jetson 硬件,请参考其文档以获取适合您的 Jetpack 版本的正确标志集。

对于基于 Docker 的部署

我们不会讨论细节,但 Docker 不仅用于开发,还用于应用程序部署。 您可以在机器人、云服务器等上运行映像实例作为独立的微服务或机器人应用程序系统。

通常来说,您可以设置“ENTRYPOINT”来启动一个脚本,该脚本为您的应用程序启动并运行服务器。 例如,您可以使用“附录”中的部署映像和“ENTRYPOINT”来启动根机器人导航启动文件“tb3_simulation_gazebo_launch.py​​”或类似文件。 您甚至可以使用“systemd”在启动时启动容器,以便在系统启动时自动启动并容器化您的应用程序。

结论

读完本章后,您现在应该能够:

  • 提取任何 ROS 发行版的官方 ROS 2 docker 镜像,并根据您的需求选择正确的镜像类型

  • 了解 ROS 2 docker 容器的格式以及“Dockerfile”镜像描述的核心部分

  • 了解 Docker 的文件系统和网络隔离 - 以及如何在开发中绕过它以实现重要的用例

  • 能够分离 docker 容器以用于长时间运行的进程

  • 将您的开发工作区安装到容器中以进行工作

  • 根据您的开发依赖项和设置需求从 ROS 构建您自己的 docker 镜像

  • 在 docker 中使用 GUI 进行可视化和模拟

此时需要注意的是,“–privileged”标志是一把真正的锤子。如果您想避免运行它,您可以找到需要启用可视化功能的所有单个区域。 还请注意,“–privileged”还可以通过启用处理这些输入的主机操作系统的输入,使运行硬件接口(如操纵杆和传感器)变得更加容易。 如果在生产中您不能使用锤子,您可能需要深入研究您的系统,以仅允许通过硬件所需的接口。

至于可能采取的措施:

  • 设置一个配置文件来隐藏所有用于开发的 docker run 参数

  • 设置一个 bash 脚本来启用 docker run 的几种不同配置并执行运行本身

  • 了解有关 Docker 的选项和功能的更多信息,例如 compose、将自己的容器推送到 DockerHub 和版本控制映像

  • 限制和调节主机资源利用率

  • 配置计算机 以避免对每个 docker CLI 命令使用 sudo

  • 考虑生产注意事项,如构建缓存管理、安全性、多阶段构建等,以充分利用 Docker

我们希望这足以帮助您入门!

——您的友好邻里导航员

附录

Nav2 部署图像

此映像可下载并安装 Nav2(滚动;从源代码)或安装它(从二进制文件),以获得运行 Nav2 所需的一切的自包含映像。 从这里,您可以转到 入门指南 进行测试!

ARG ROS_DISTRO=rolling
FROM ros:${ROS_DISTRO}-ros-core

RUN apt update \
    && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends --no-install-suggests \
  ros-dev-tools \
  wget

# For Rolling or want to build from source a particular branch / fork
WORKDIR /root/nav2_ws
RUN mkdir -p ~/nav2_ws/src
RUN git clone https://github.com/ros-planning/navigation2.git --branch main ./src/navigation2
RUN rosdep init
RUN apt update && apt upgrade -y \
    && rosdep update \
    && rosdep install -y --ignore-src --from-paths src -r
RUN . /opt/ros/${ROS_DISTRO}/setup.sh \
    && colcon build --symlink-install

# For all else, uncomment the above Rolling lines and replace with below
# RUN rosdep init
# RUN apt update && apt upgrade -y \
#     && rosdep update \
#     && apt install \
#         ros-${NAV2_BRANCH}-nav2-bringup \
#         ros-${NAV2_BRANCH}-navigation2 \
#         ros-${NAV2_BRANCH}-turtlebot3-gazebo