基于已有的Dockerfile用 Docker 搭建 ROS2 开发环境
从零开始:基于已有的Dockerfile用 Docker 搭建 ROS2 开发环境
之前我曾写过一篇关于 Docker 使用方法的技术博客。在实际使用的过程中发现,如果只是手动运行容器并逐步配置环境,其实并不是最方便的方式。相比之下,使用 Dockerfile 来统一构建开发环境会更加清晰和高效。因此我将自己常用的环境进行了整理,并封装成了一个 Dockerfile,在这里分享给大家。目前该 Dockerfile 主要用于快速搭建 ROS2 Humble 开发环境,如果需要安装其他版本或扩展不同功能,只需要在此基础上进行相应修改即可。接下来将对整个配置过程进行详细介绍。
Dockerfile 文档下载链接:
https://download.csdn.net/download/cherishingzx/92709806?spm=1011.2124.3001.6210
目录
- 先搞清楚几个概念
- 安装 Docker
- 安装 Docker Compose
- 安装 make(可选但推荐)
- 验证安装
- 认识项目里的文件
- 构建镜像
- 启动容器
- 进入容器
- 在容器里构建 ROS 工作空间
- 什么是 —symlink-install
- 运行示例、验证通信
- 常见报错和解决方法
- 偷懒技巧:让环境自动生效
- 命令速查表
1. 先搞清楚几个概念
刚接触 Docker 的时候,我也被各种术语搞晕了。这里用大白话解释一下,看完就够用了。
Docker —— 一个”轻量虚拟机”工具。它不是真的虚拟机(不模拟硬件),而是用 Linux 内核的隔离功能,把一套完整的运行环境(操作系统 + 库 + 工具)打包在一起。你可以理解成:一个独立的、干净的、可复现的小系统,跑在你的 Linux 里面。
镜像(Image) —— 一个只读的”模板”。类比成操作系统安装盘 ISO,或者游戏安装包。你不能直接在里面干活,但可以用它创建出可以干活的环境。
容器(Container) —— 用镜像启动出来的”实例”。类比成你用安装盘装好的一台”电脑”,可以往里面写文件、跑程序。一个镜像可以启动很多个容器,互不影响。
Dockerfile —— 构建镜像的”菜单”。告诉 Docker”先装什么、再装什么、复制哪些文件进去”。写好之后执行一次 docker compose build,就会按菜单做出一个镜像。
docker-compose.yml —— 启动容器的”配置表”。告诉 Docker”用哪个镜像、挂载哪些目录、开放哪些权限”。不用你手动敲一长串 docker run 参数。
挂载(Volume Mount) —— 把宿主机上的目录”映射”到容器里。比如我们把项目目录映射到容器里的 /workspace,这样你在宿主机用 VS Code 改代码,容器里立刻就能看到,反过来容器里的输出宿主机上也能看到。
2. 安装 Docker
以下步骤适用于 Ubuntu 20.04 / 22.04 / 24.04。其他发行版大同小异,命令可能略有不同。
一行一行复制粘贴就行:
1 | # 删除旧版本(如果之前装过),没装过的话这步会提示"没找到",无所谓 |
装完之后让当前用户可以直接用 docker(不用每次加 sudo):
1 | sudo usermod -aG docker $USER |
这一步完了要重新登录(或者重启电脑),否则不生效。 简单粗暴的方法是直接重启:
1 | sudo reboot |
3. 安装 Docker Compose
好消息:如果你按上面的命令安装的,Docker Compose v2 已经装好了(docker-compose-plugin 这个包自带了)。
验证一下:
1 | docker compose version |
如果输出类似 Docker Compose version v2.x.x,就说明可以了。
新版 vs 旧版的区别:
- 新版(v2):命令是
docker compose(中间是空格),这是现在推荐的方式。- 旧版(v1):命令是
docker-compose(中间是横杠),已经不维护了。本文用的全是新版
docker compose。如果你在网上看到别人写的是docker-compose(带横杠),直接把横杠换成空格就行。
4. 安装 make(可选但推荐)
项目里有个 Makefile,可以让你用 make build、make up、make exec 这种短命令代替一长串 docker compose ...。不是必须的,但确实方便。
1 | sudo apt-get install -y make |
5. 验证安装
全部装完、重启过之后,打开终端确认一下:
1 | docker --version # 应该输出 Docker version 2x.x.x |
三条都正常就说明 Docker 环境没问题了。hello-world 那行会自动下载一个很小的测试镜像跑一下,跑完就结束。
如果 docker run hello-world 报权限错误,说明你没重新登录或者重启。退出登录重新进就好了。
6. 认识项目里的文件
在干活之前,先看看项目根目录下每个文件是干嘛的:
1 | 项目根目录/ |
下面逐个说说。
.env 文件
docker compose 启动时会自动读取这个文件里的变量,注入到 docker-compose.yml 里使用。内容长这样:
1 | UID=1000 |
每一行的意思:
UID/GID:你的 Linux 用户 ID 和组 ID。容器里会创建一个同 ID 的用户,这样容器里生成的文件在宿主机上也是你的,不会变成 root 的(文件权限不会乱)。怎么查自己的?终端执行id就能看到。ROS_DOMAIN_ID:ROS2 用来隔离不同”网络域”的编号。同一台机器上跑多个项目就用不同的 ID,只跑一个的话写 0 就行。DISPLAY:告诉容器把 GUI 窗口显示到宿主机的哪个显示器上,一般不用改。COMPOSE_PROJECT_NAME:给项目起个名字,docker compose 用这个来命名容器和网络。
docker-compose.yml
这个文件定义了容器怎么启动。里面值得注意的几个配置:
network_mode: host—— 容器直接用宿主机的网络,不做网络隔离。对 ROS2 来说这样最简单,节点之间直接能通信。volumes: ./:/workspace—— 把当前项目目录挂载到容器里的/workspace。你在宿主机改的代码,容器里立刻能看见。user: "${UID}:${GID}"—— 让容器里的进程以你的用户身份运行,而不是 root。privileged: true—— 给容器完整的设备访问权限(GPU、传感器等需要)。shm_size: 8g—— 共享内存设为 8GB,跑仿真、深度学习之类的需要大一点。
entrypoint.sh
容器每次启动时最先运行的脚本。它干两件事:
- 加载 ROS2 的环境变量(
source /opt/ros/humble/setup.bash),让容器里能用ros2命令。 - 如果你已经构建过工作空间(
/workspace/install/setup.bash存在),也自动加载它。
Dockerfile
最核心的文件。定义了镜像里都装了什么。大致流程:
- 以
osrf/ros:humble-desktop为基础镜像(这是 ROS 官方维护的 Ubuntu + ROS2 Humble 桌面版)。 - 用
apt-get安装开发工具(git、vim、tmux、cmake、gdb 等)和依赖库。 - 用
pip装 Python 包(numpy 等)。 - 创建一个非 root 用户,ID 和宿主机对齐。
- 设置好 ROS2 环境自动加载。
- 拷贝
entrypoint.sh,设为启动入口。
你不需要改这个文件就能用。但如果你发现容器里缺某个系统包,在 apt-get install 那一段加一行,然后重新 docker compose build 就行了。
7. 构建镜像
第一次构建大概 5-15 分钟,取决于网速和机器性能。后续再 build(Dockerfile 没大改的话)会快很多,因为有缓存。
终端 cd 到项目根目录(就是 docker-compose.yml 所在的目录),然后:
1 | docker compose build |
或者用 Makefile 快捷方式:
1 | make build |
这两条效果完全一样。make build 底层就是调用 docker compose build。
如果你改了 Dockerfile 想从头来一遍(不用缓存):
1 | docker compose build --no-cache |
构建完成后你会有一个叫 pnd_u:latest 的本地镜像。验证一下:
1 | docker images | grep pnd_u |
能看到一行输出就说明成功了。
8. 启动容器
启动之前先给 X11 授权——这步是让容器里的 GUI 程序(比如 rviz、MuJoCo 查看器)能把窗口显示在你的屏幕上:
1 | xhost +local:docker |
这条命令每次开机后运行一次就够了。它告诉你的桌面系统”允许本地 Docker 容器往屏幕上画窗口”。没什么安全风险,只是允许本机的 docker 访问 X11。
然后启动容器:
1 | docker compose up -d |
或者:
1 | make up # 这个会自动帮你先执行 xhost,然后再 up |
-d 是 detach(分离)的意思,让容器在后台运行,不占着你当前的终端。
检查一下有没有跑起来:
1 | docker compose ps |
你应该看到一行 pnd_u,状态(STATUS)显示 Up。如果不是 Up,看看日志找原因:
1 | docker compose logs |
9. 进入容器
容器跑起来之后,你需要”进去”打开一个终端才能干活:
1 | docker compose exec ros2 bash |
或者:
1 | make exec |
进去之后终端提示符会变(比如变成 developer@你的主机名:/workspace$),说明你已经在容器里面了。
这条命令拆开看:
docker compose exec—— 在一个正在运行的容器里执行命令ros2—— docker-compose.yml 里定义的服务名bash—— 要执行的命令,就是打开一个交互终端
可以多开! 你可以同时开好几个终端窗口,每个都执行一次 docker compose exec ros2 bash,它们都在同一个容器里,互不影响。跑仿真一个窗口、跑控制器一个窗口、查话题一个窗口。
怎么退出? 输入 exit 或者按 Ctrl+D。这只是关掉当前这个 shell,容器本身还在后台跑着,不受影响。
10. 在容器里构建 ROS 工作空间
进入容器后,执行:
1 | cd /workspace |
三行命令,逐个解释:
cd /workspace—— 切到工作空间根目录。你的项目代码就挂载在这里(对应宿主机上的项目根目录)。colcon build --symlink-install—— 构建所有 ROS 包。colcon是 ROS2 的构建工具,会自动扫描目录里的package.xml文件,找到所有 ROS 包,然后编译。--symlink-install的详细解释见下一节。source install/setup.bash—— 让当前终端”认识”刚才编译出来的包。比如你定义了自定义消息类型,不 source 这个文件的话 Python 里 import 会报错。
非常重要:每次你新开一个终端进入容器,都需要执行 source install/setup.bash。 因为每个新 shell 是一个新的进程,环境变量不会自动继承。后面第 14 节会教你怎么让它自动执行。
11. 什么是 —symlink-install
这个参数值得单独讲一下,因为一开始我也不太明白。
不加这个参数的时候
colcon build 会把你的源码文件复制一份到 install/ 目录下。系统运行时用的是 install 里的那个副本,不是你的原始源文件。
这意味着:你改了 Python 代码之后,必须重新执行 colcon build,改动才能生效。 因为 install 里的还是旧的副本。
加了 —symlink-install 之后
install 目录里放的不是副本,而是符号链接(symlink),也就是”快捷方式”,直接指向你的原始源文件。
这意味着:你改了 Python 代码,保存,马上就能直接运行。 不需要重新 build。因为 install 里的”快捷方式”指向的就是你刚改的那个文件。
打个比方
- 不加
--symlink-install= 你把文件复印了一份放到另一个抽屉里,别人从那个抽屉拿。你改了原件,抽屉里的还是旧的,得重新复印。 - 加了
--symlink-install= 你在那个抽屉里放了一张纸条写着”去原来那个柜子拿”。你改了原件,别人顺着纸条拿到的就是新的。
哪些改动不需要重新 build
- 改了已有 Python 文件(
.py)的逻辑内容 - 改了 launch 文件、参数 YAML 等配置文件
哪些改动必须重新 build
- 改了消息/服务定义(
.msg、.srv、.action)—— 这些文件需要代码生成器处理,生成对应的 Python/C++ 接口代码 - 新增了一个包(新的目录 + 新的
package.xml)—— colcon 需要重新扫描和注册 - 改了 C++ 源文件(
.cpp、.hpp)—— C++ 是编译型语言,改了就得重新编译 - 改了构建配置文件(
CMakeLists.txt、setup.py、setup.cfg)
什么时候不用 —symlink-install
如果是用来做最终打包发布的,不需要这个参数。直接 colcon build 会把文件完整复制到 install,打包出来的东西是独立完整的。但开发阶段基本上都会加 --symlink-install,省得每次改代码还要等 build。
12. 运行示例、验证通信
一般我会开 3 个终端窗口,都进入到容器里。
终端 1 —— 跑仿真
1 | docker compose exec ros2 bash |
终端 2 —— 跑控制器
1 | docker compose exec ros2 bash |
程序会提示按 Enter 开始,按一下就行。
终端 3 —— 看话题,确认通信
1 | docker compose exec ros2 bash |
你应该能看到 /lowcmd 和 /lowstate 之类的话题。再看看详细信息:
1 | ros2 topic info /lowstate |
如果看到 Publisher count 和 Subscription count 都大于 0,说明仿真和控制器之间的通信已经建立了。
想看实时数据也行:
1 | ros2 topic echo /lowstate |
会不停地打印消息内容,看完按 Ctrl+C 退出。
13. 常见报错和解决方法
下面这些坑我都踩过,全部记在这里。
13.1 ros2: command not found
原因: ROS2 的环境变量没加载。每个新 shell 都需要 source 一下。
解决:
1 | source /opt/ros/humble/setup.bash |
如果你已经 build 过工作空间,再加一句:
1 | source /workspace/install/setup.bash |
为什么要 source? source 命令会在当前 shell 里执行一个脚本文件,那个脚本里设置了一堆路径变量(PATH、PYTHONPATH、AMENT_PREFIX_PATH 等等),告诉系统 ros2 命令在哪、Python 包在哪。这些变量只在当前 shell 生效,所以每个新开的终端都要 source 一次。
13.2 ModuleNotFoundError: No module named 'xxx'
常见的有 pnd_adam、pndbotics_sdk_py 之类。
原因 A: 没有执行 colcon build,或者 build 的时候没有包含这个包。
1 | cd /workspace |
原因 B: build 了但是没 source install/setup.bash。
1 | source /workspace/install/setup.bash |
原因 C: 你想装的是一个纯 Python 库(不是 ROS 包),比如某个 SDK,那就用 pip 装:
1 | pip3 install -e /workspace/path/to/that/package |
-e 是 editable 模式,道理和 --symlink-install 类似——改了源码不用重装。
13.3 两个节点看不到对方的话题
在两个终端分别执行:
1 | echo $ROS_DOMAIN_ID |
ROS_DOMAIN_ID 必须一致。 这个 ID 就像无线电的频道号,两个节点在不同频道上就互相听不见。
RMW_IMPLEMENTATION 推荐也保持一致。 通常设为 rmw_cyclonedds_cpp。
统一设置:
1 | export ROS_DOMAIN_ID=0 |
然后再启动你的节点。
13.4 fatal error: numpy/ndarrayobject.h: No such file or directory
colcon 编译 ROS 消息的 Python 绑定时会用到 numpy 的 C 头文件。
解决:
1 | sudo apt-get update |
然后重新 colcon build。本项目的 Dockerfile 已经装好了,一般不会遇到。但如果你自己弄了个新环境就可能碰到。
13.5 libGL error: failed to load driver
跑 rviz、MuJoCo 之类的 GUI 程序时可能报这个。
原因: 显卡驱动没装好,或者设备没映射到容器里。
排查:
- 宿主机执行
nvidia-smi(N 卡)或者glxinfo | grep OpenGL(核显),看看驱动是不是正常的。 - 确认
docker-compose.yml里有/dev/dri:/dev/dri的映射。
这个报错不影响 ROS2 通信本身,只影响图形界面显示。
13.6 话题名报错 Unknown topic
1 | # 错误写法 |
ROS2 的话题名都是以 / 开头的绝对路径,别忘了。
14. 偷懒技巧:让环境自动生效
每次进容器都要手动敲 source install/setup.bash 很烦。下面两个方法任选一个就行。
方法一:往 .bashrc 里加一行
在宿主机执行一次这条命令(只需要执行一次):
1 | docker compose exec ros2 bash -c 'grep -q "install/setup.bash" ~/.bashrc || echo "[ -f /workspace/install/setup.bash ] && source /workspace/install/setup.bash" >> ~/.bashrc' |
这条命令干的事:检查容器里用户的 ~/.bashrc 文件,如果还没有 source 那行就加上去。
加上之后,以后每次 docker compose exec ros2 bash 进去的时候,ROS 工作空间的环境变量就自动加载好了,不用手动 source 了。
方法二:改 Makefile(如果你用 make)
把 Makefile 里的 exec 部分改成这样:
1 | exec: |
这样 make exec 进去的 shell 自动就有 overlay 环境了。 2>/dev/null 是为了在文件不存在时(比如还没 build 过)不报错。
15. 命令速查表
平时最常用的命令,方便回来查。
宿主机上的命令
| 干什么 | 完整命令 | Makefile 版 |
|---|---|---|
| 构建镜像 | docker compose build |
make build |
| 无缓存重建 | docker compose build --no-cache |
make rebuild |
| 启动容器 | docker compose up -d |
make up |
| 停止容器 | docker compose down |
make down |
| 进入容器 | docker compose exec ros2 bash |
make exec |
| 看容器状态 | docker compose ps |
make ps |
| 看日志 | docker compose logs -f --tail=100 |
make logs |
| 授权 X11 | xhost +local:docker |
make xhost |
| 重启容器 | 先 down 再 up | make restart |
| 清理全部 | docker compose down -v --rmi local |
make clean |
容器里的命令
1 | # 构建工作空间 |
整个流程总结起来就是:装 Docker → docker compose build → docker compose up -d → docker compose exec ros2 bash → colcon build → 跑你的程序。中间遇到什么问题翻翻上面的常见报错,基本都能自己搞定。