Dev 容器指南
在本指南中,我们将逐步讲解为项目创建和使用 dev 容器的过程。虽然各个小节会提供更详细的说明,但理解整篇指南并非必须,只需根据自己的需求使用即可。不过,如果你对 dev 容器的工作原理感兴趣,或者想定制和优化它们以适应自己的工作流,建议通读本指南。
创建 Dev 容器
在创建 dev 容器之前,首先需要选择具体的配置。默认情况下,会选择 .devcontainer/devcontainer.json
配置文件,但你也可以选择 .devcontainer/
目录中的任何其他 devcontainer.json
文件,其中配置可以嵌套,提供更高的定制化:比如针对不同的 Dockerfile 阶段、覆盖合并的元数据或默认属性,或者包含额外的扩展和命令。
See also
devcontainer.json
配置文件格式的规范、参考和架构可以在以下链接中找到:
-
开发容器规范
-
元数据和属性参考
-
devcontainer.json
的 JSON 架构
构建镜像
首次创建 dev 容器时,所需的支持工具或服务会下载并构建运行容器所需的 Docker 镜像。这包括拉取项目 Dockerfile 中 FROM
声明的父镜像,以及 devcontainer.json
文件中通过 cacheFrom
声明的标签或层。这可能需要一些时间,但只需执行一次,除非某些层被更新并推送到镜像注册表。
具体来说,对于本项目,默认的 devcontainer.json
文件定位于项目根目录 Dockerfile 中的 dever
阶段,该阶段包含开发项目时所需的工具,例如 Bash 自动补全功能。这个阶段FROM
builder
阶段构建,后者仅包含构建项目所需的依赖项,这也是项目 CI 重用的部分。
为了加快初始构建,builder
阶段的镜像层会从项目 CI 使用的相同镜像标签缓存,从镜像仓库拉取。这确保了本地 Dev 容器尽可能接近 CI 环境,同时受益于 CI 预先完成的任何缓存工作。
Once the base image from the target stage is built, the supporting tool or service may then add additional layers to the image, such as installing additional features or customizations. For VS Code, this also includes some fancy file caching for any extensions to install later. Once this custom image is built, it is then used to start the dev container. 一旦目标阶段的基础镜像构建完成,支持工具或服务可能会在镜像上添加额外的层,例如安装附加功能或自定义内容。对于 VS Code,这也包括为稍后安装的扩展进行文件缓存
启动容器
在首次创建 Dev 容器时,支持的工具或服务将调用 devcontainer.json
配置文件中指定的命令序列。这可能需要一段时间,但只需执行一次,除非 Dockerfile、基础镜像或 .devcontainer/
配置更新,从而触发容器重建。
具体来说,本项目的默认 devcontainer.json
配置文件执行了 onCreateCommand
,最初对工作空间进行缓存、清理和构建,确保工作空间预编译并可用,同时确保对项目源代码的任何更改都反映在容器中。
智能感知
允许VS代码扩展解析自动生成的代码
适用于ROS包定义消息和服务文件
代码建模、导航和语法突出显示所必需
缓存
启用Codespace预构建以缓存工作区工件
适用于减少生成新代码空间时的启动时间
限制CPU和存储使用成本所必需的
接下来,执行 updateContentCommand
,在每次启动或重启容器时重新运行。该命令通过缓存行为复制项目的 CI 工作流程。
分支机构
在分支之间切换时启用工作区工件的缓存
适用于在不重建整个容器的情况下审查拉取请求
生成新代码空间时减少启动时间所必需的
Hint
关于这些额外的colcon动词扩展的更多文档可以在这里找到:
-
一个用于缓存包处理的 colcon 扩展
-
一个用于清理包工作区的 colcon 扩展
最后,postCreateCommand
会被执行,并且每次启动或重启容器时也会重新运行。具体来说,在这个项目中,此命令对用户的环境做最后的优化,以改善开发体验。
为了加速后续的启动,挂载到容器的卷会存储持久的 ccache 和 colcon 工作区,并通过 colcon mixins 设置环境来启用 ccache。这些卷使用 devcontainerId
变量进行标记,能够唯一标识 Docker 主机上的 dev 容器,使我们可以引用一个独特的标识符,在多次重建过程中保持稳定。这对于以下情况非常有用:
缓存
使 colcon 工作区和 ccache 在容器重建之间持久化
可避免在修改 dev 容器配置文件时重新编译
适用于快速定制镜像或功能,而无需从头重建
Tip
虽然这些卷名称是唯一的,但你可以在本地重命名它们,以进一步组织或分割进行中的工作。例如,在卷名中添加分支名,以便快速在不同的 pull request 和缓存的 colcon 工作区之间切换。
此外,容器可以授予privileged和非默认的 Linux capabilities,连接主机网络模式和 IPC、PID 空间,并通过放松的security configuration和 seccomp 限制进行本地调试和外部连接。这对以下场景很有用:
混合开发
允许连接外部 ROS 节点到容器
适用于调试或可视化分布式系统
对于 DDS 发现和共享内存传输是必需的
设备连接
允许将硬件从主机转发到容器
适用于使用传感器和执行器的 ROS 包
对于某些 GPU 驱动程序和 USB 设备是必需的
Attention
注意:可以在 devcontainer.json
配置中启用或自定义这些 runArgs
参数,根据需要扩大或缩小其范围,以更好地适应您的开发环境。默认配置仅将这些参数注释掉,以限制容器之间的意外副作用或干扰,但可以取消注释以适应更广泛的开发使用场景。
See also
关于在 Docker 容器中使用 DDS、调试器或设备的更多详细信息,请参见以下内容:
-
使用
host
网络驱动程序从 Docker 容器访问主机的所有网络接口
使用 DDS 和共享内存在两个 Docker 容器之间进行通信
使容器之间以及容器与主机之间能够使用进程间通信(IPC)进行通信
-
在容器中调试程序时使用 strace、perf、gdb 等工具
使用开发容器
一旦开发容器创建并设置完成,VS Code 将直接从项目根目录打开一个新的工作区,该目录本身已挂载到源目录中的 overlay colcon 工作区。从这里,您可以像平常一样构建、测试和调试项目,同时还享有已预先配置好的依赖项、智能感知(Intellisense)、代码检查(linters)和其他扩展的好处。只需打开一个新的终端(Ctrl+Shift+`),进入 colcon 工作区的根目录,然后运行常规的 colcon 命令。
Tip
您可以结合使用 devcontainer.json
配置文件中的相同脚本,以进一步自动化本地开发工作流程。
终端
如果您更喜欢使用其他终端仿真器,而不是 VS Code 内置的终端,您可以通过 Dev Container CLI 或直接通过 Docker CLI 使用 exec
子命令来打开一个单独的 shell 会话。
-
devcontainer exec --workspace-folder $NAV2_WS/src/navigation2 bash
-
docker exec -it <container-id> bash
Attention
通过 docker exec
启动的 shell 会话不会像 devcontainer exec
使用 userEnvProbe
那样设置相同的环境变量。额外的环境变量包括 REMOTE_CONTAINERS_IPC
, REMOTE_CONTAINERS_SOCKETS
,这些变量由 VS Code、SSH 和 X11 使用。
Hint
由 userEnvProbe
提供的环境变量可以手动加载。例如,对于默认的 loginInteractiveShell
探针:
find /tmp -type f -path "*/devcontainers-*/env-loginInteractiveShell.json" -exec \
jq -r 'to_entries | .[] | "\(.key)=\(.value | @sh)"' {} \; > .env
source .env
生命周期
在使用开发容器时,请牢记容器本身的生命周期。容器是临时的,这意味着每当开发环境重建或更新时,容器通常会被销毁并重新创建。因此,最佳实践是避免将任何持久性数据存储在容器内,而应使用项目的源目录或单独挂载的卷。 当你在容器内更改开发环境时,记得将这些更改写入 Dockerfile 或 devcontainer.json
配置文件,以便能够轻松复现并与他人共享。
Important
当主机本身也是临时性的时,这一点尤其重要,比如在使用基于云的环境(如Codespaces)时。因此,务必将本地更改提交并推送到远程存储库。
-
在整个Codespace生命周期内维护您的数据。
重建
有时,您可能需要重建开发容器,要么是因为基础映像或.devcontainer/
配置已更新,要么只是因为想要一个新的开发环境。为此,只需打开命令面板(Ctrl+Shift+P)并选择Remote-Containers: Rebuild Container
命令。
Caution
重建容器会删除对容器本身所做的任何更改,例如安装的额外软件包或修改的环境配置。然而,项目的源目录和任何挂载的卷不会受到影响。
例如,您可能需要在以下情况下重建开发容器:
从容器注册表中拉取更新的镜像
特别是 Dockerfile 中使用的
FROM
标签镜像或
devcontainer.json
中列出的cacheFrom
标签定期手动执行以确保本地环境与持续集成(CI)保持一致
更新开发容器配置
尤其是修改
Dockerfile
中依赖的构建阶段或修改
./devcontainer
文件和命令时构建缓存的重用取决于所做更改的程度
在必要时,您还可以从头重建容器,即不使用 Docker 缓存,通过选择Remote-Containers: Rebuild Container Without Cache
命令。这会从 docker buildx
命令中省略 --cache-from
标志,并添加 --no-cache
和 --pull
标志,以防止使用任何现有镜像层的缓存,确保仅使用来自容器注册表的最新镜像。
Caution
无缓存重建容器可能会从容器注册表中拉取更新的镜像或安装新软件包,这在开发 ROS 2 Rolling 时很常见。因此,您可能需要清理叠加卷以避免 ABI 不兼容或过时的工件。
无缓存重建可能在以下情况下是必要的:
需要更新基础镜像
特别是在开发容器配置保持不变的情况下
强制重新运行 Dockerfile 中的
RUN
指令如未更改的
apt upgrade
或rosdep update
命令
特别是对于这个项目,卷不会受到此重建过程的影响,例如用于挂载 ccache 目录或 colcon 工作区的卷。虽然卷管理由用户自行决定,但其他项目可能会有所不同,因此请检查 ./devcontainer
配置以了解容器资源的管理方式。
Tip
可以通过 Docker CLI 或 VS Code Docker 扩展来管理 Docker 卷:
-
使用子命令创建、检查、列出、删除或清理卷
-
使您可以轻松创建、管理和调试容器化应用