从零开始:基于已有的Dockerfile用 Docker 搭建 ROS2 开发环境

之前我曾写过一篇关于 Docker 使用方法的技术博客。在实际使用的过程中发现,如果只是手动运行容器并逐步配置环境,其实并不是最方便的方式。相比之下,使用 Dockerfile 来统一构建开发环境会更加清晰和高效。因此我将自己常用的环境进行了整理,并封装成了一个 Dockerfile,在这里分享给大家。目前该 Dockerfile 主要用于快速搭建 ROS2 Humble 开发环境,如果需要安装其他版本或扩展不同功能,只需要在此基础上进行相应修改即可。接下来将对整个配置过程进行详细介绍。

Dockerfile 文档下载链接:
https://download.csdn.net/download/cherishingzx/92709806?spm=1011.2124.3001.6210


目录

  1. 先搞清楚几个概念
  2. 安装 Docker
  3. 安装 Docker Compose
  4. 安装 make(可选但推荐)
  5. 验证安装
  6. 认识项目里的文件
  7. 构建镜像
  8. 启动容器
  9. 进入容器
  10. 在容器里构建 ROS 工作空间
  11. 什么是 —symlink-install
  12. 运行示例、验证通信
  13. 常见报错和解决方法
  14. 偷懒技巧:让环境自动生效
  15. 命令速查表

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 删除旧版本(如果之前装过),没装过的话这步会提示"没找到",无所谓
sudo apt-get remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true

# 安装依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg

# 添加 Docker 官方 GPG 密钥
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# 添加 Docker 软件源
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 安装 Docker Engine + CLI + 插件
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

装完之后让当前用户可以直接用 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 buildmake upmake exec 这种短命令代替一长串 docker compose ...。不是必须的,但确实方便。

1
sudo apt-get install -y make

5. 验证安装

全部装完、重启过之后,打开终端确认一下:

1
2
3
docker --version          # 应该输出 Docker version 2x.x.x
docker compose version # 应该输出 Docker Compose version v2.x.x
docker run hello-world # 应该打印一段 Hello from Docker!

三条都正常就说明 Docker 环境没问题了。hello-world 那行会自动下载一个很小的测试镜像跑一下,跑完就结束。

如果 docker run hello-world 报权限错误,说明你没重新登录或者重启。退出登录重新进就好了。


6. 认识项目里的文件

在干活之前,先看看项目根目录下每个文件是干嘛的:

1
2
3
4
5
6
7
8
项目根目录/
├── .env ← 环境变量(UID、GID、ROS 域名等)
├── docker-compose.yml ← 容器启动配置
├── Makefile ← 快捷命令(make build / make up / make exec …)
└── docker/
├── Dockerfile ← 镜像构建菜单
├── entrypoint.sh ← 容器启动时的初始化脚本
└── .dockerignore ← 构建时忽略的文件列表

下面逐个说说。

.env 文件

docker compose 启动时会自动读取这个文件里的变量,注入到 docker-compose.yml 里使用。内容长这样:

1
2
3
4
5
6
UID=1000
GID=1000
USER_NAME=developer
ROS_DOMAIN_ID=0
DISPLAY=${DISPLAY:-:0}
COMPOSE_PROJECT_NAME=pnd_u

每一行的意思:

  • 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

容器每次启动时最先运行的脚本。它干两件事:

  1. 加载 ROS2 的环境变量(source /opt/ros/humble/setup.bash),让容器里能用 ros2 命令。
  2. 如果你已经构建过工作空间(/workspace/install/setup.bash 存在),也自动加载它。

Dockerfile

最核心的文件。定义了镜像里都装了什么。大致流程:

  1. osrf/ros:humble-desktop 为基础镜像(这是 ROS 官方维护的 Ubuntu + ROS2 Humble 桌面版)。
  2. apt-get 安装开发工具(git、vim、tmux、cmake、gdb 等)和依赖库。
  3. pip 装 Python 包(numpy 等)。
  4. 创建一个非 root 用户,ID 和宿主机对齐。
  5. 设置好 ROS2 环境自动加载。
  6. 拷贝 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
2
3
docker compose build --no-cache
# 或者
make rebuild

构建完成后你会有一个叫 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
2
3
cd /workspace
colcon build --symlink-install
source install/setup.bash

三行命令,逐个解释:

  1. cd /workspace —— 切到工作空间根目录。你的项目代码就挂载在这里(对应宿主机上的项目根目录)。

  2. colcon build --symlink-install —— 构建所有 ROS 包。colcon 是 ROS2 的构建工具,会自动扫描目录里的 package.xml 文件,找到所有 ROS 包,然后编译。--symlink-install 的详细解释见下一节。

  3. source install/setup.bash —— 让当前终端”认识”刚才编译出来的包。比如你定义了自定义消息类型,不 source 这个文件的话 Python 里 import 会报错。

非常重要:每次你新开一个终端进入容器,都需要执行 source install/setup.bash 因为每个新 shell 是一个新的进程,环境变量不会自动继承。后面第 14 节会教你怎么让它自动执行。


这个参数值得单独讲一下,因为一开始我也不太明白。

不加这个参数的时候

colcon build 会把你的源码文件复制一份到 install/ 目录下。系统运行时用的是 install 里的那个副本,不是你的原始源文件。

这意味着:你改了 Python 代码之后,必须重新执行 colcon build,改动才能生效。 因为 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.txtsetup.pysetup.cfg

如果是用来做最终打包发布的,不需要这个参数。直接 colcon build 会把文件完整复制到 install,打包出来的东西是独立完整的。但开发阶段基本上都会加 --symlink-install,省得每次改代码还要等 build。


12. 运行示例、验证通信

一般我会开 3 个终端窗口,都进入到容器里。

终端 1 —— 跑仿真

1
2
3
4
docker compose exec ros2 bash
source /workspace/install/setup.bash
cd /workspace/pnd_mujoco/simulate_python
python3 pnd_mujoco.py

终端 2 —— 跑控制器

1
2
3
4
docker compose exec ros2 bash
source /workspace/install/setup.bash
cd /workspace/pnd_ros2/example/adam_u
python3 adam_u_low_level_example.py

程序会提示按 Enter 开始,按一下就行。

终端 3 —— 看话题,确认通信

1
2
3
docker compose exec ros2 bash
source /workspace/install/setup.bash
ros2 topic list

你应该能看到 /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 里执行一个脚本文件,那个脚本里设置了一堆路径变量(PATHPYTHONPATHAMENT_PREFIX_PATH 等等),告诉系统 ros2 命令在哪、Python 包在哪。这些变量只在当前 shell 生效,所以每个新开的终端都要 source 一次。

13.2 ModuleNotFoundError: No module named 'xxx'

常见的有 pnd_adampndbotics_sdk_py 之类。

原因 A: 没有执行 colcon build,或者 build 的时候没有包含这个包。

1
2
3
cd /workspace
colcon build --symlink-install
source install/setup.bash

原因 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
2
echo $ROS_DOMAIN_ID
echo $RMW_IMPLEMENTATION

ROS_DOMAIN_ID 必须一致。 这个 ID 就像无线电的频道号,两个节点在不同频道上就互相听不见。

RMW_IMPLEMENTATION 推荐也保持一致。 通常设为 rmw_cyclonedds_cpp

统一设置:

1
2
export ROS_DOMAIN_ID=0
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp

然后再启动你的节点。

13.4 fatal error: numpy/ndarrayobject.h: No such file or directory

colcon 编译 ROS 消息的 Python 绑定时会用到 numpy 的 C 头文件。

解决:

1
2
3
sudo apt-get update
sudo apt-get install -y python3-dev python3-numpy
pip3 install numpy

然后重新 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
2
3
4
5
# 错误写法
ros2 topic info lowstate

# 正确写法(前面要加斜杠)
ros2 topic info /lowstate

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
2
exec:
docker compose exec ros2 bash -c "source /workspace/install/setup.bash 2>/dev/null; exec bash"

这样 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 构建工作空间
cd /workspace
colcon build --symlink-install

# 加载环境(每个新 shell 都要,除非你配了自动加载)
source install/setup.bash

# 查看当前有哪些话题
ros2 topic list

# 查看某个话题的详情(发布者/订阅者数量、消息类型)
ros2 topic info /lowstate

# 实时查看话题数据
ros2 topic echo /lowstate

# 查看当前有哪些节点在运行
ros2 node list

# 只编译某一个包(比改动不大时更快)
colcon build --symlink-install --packages-select 包名

整个流程总结起来就是:装 Docker → docker compose builddocker compose up -ddocker compose exec ros2 bashcolcon build → 跑你的程序。中间遇到什么问题翻翻上面的常见报错,基本都能自己搞定。