01:搭建 ROS1 Noetic Docker 开发环境
摘要:这一章只做一件事——把 Ubuntu 24.04 Host、Docker、Ubuntu 20.04 Focal 和 ROS1 Noetic 组合成一个稳定、可重复的开发环境。完成后,你应该能解释 Dockerfile、Image、Container、Compose、bind mount、
sleep infinity和/ros_entrypoint.sh各自负责什么,并能手工启动、进入、停止开发 Container。
@[toc]
本章目标
完成本章后,你应该能够:
- 解释为什么 Host 可以是 Ubuntu 24.04,而 ROS1 Noetic 仍运行在 Ubuntu 20.04 Focal userspace 中;
- 区分 Dockerfile、Image、Container 和 Compose;
- 理解为什么整个仓库 bind mount 到 Container 的
/workspace; - 理解为什么开发 Container 使用普通用户
dev; - 理解
network_mode: host、init: true、sleep infinity的作用; - 用
docker compose手工完成 build、up、ps、exec、logs、stop、down; - 知道官方 ROS Image 的
/ros_entrypoint.sh为什么会自动 source/opt/ros/noetic/setup.bash。
为什么先搭环境,而不是直接写 ROS 代码
ROS1 Noetic 的目标平台是 Ubuntu 20.04 Focal。你的 Host 可以运行更现代的 Ubuntu,但如果直接在 Host 上混装 Noetic、旧版依赖、调试工具和业务代码,后续会出现两个问题:
- Host 系统升级会影响 ROS 开发环境;
- 不同开发者机器之间很难保持一致。
Docker 的价值不是“把 ROS 变成虚拟机”,而是把 ROS 所需要的 userspace 固定下来。
本工程采用:
1 | Ubuntu 24.04 Host |
Container 和 Host 共享 Linux Kernel,但文件系统、进程视图、系统库和安装的软件由 Container 自己的 userspace 提供。
因此:
1 | Host 是 Ubuntu 24.04 |
和:
1 | Container 里是 Ubuntu 20.04 + Noetic |
并不矛盾。
Step 1:先看清工程根目录
Host 上进入仓库:
1 | cd /home/wdfk/share/ros1-docker |
这一章最重要的几个文件是:
1 | ros1-docker/ |
先记住一个路径映射:
1 | Host: |
后续看到:
1 | /workspace/ros_ws |
就要立刻想到它实际上对应 Host 的:
1 | /home/wdfk/share/ros1-docker/ros_ws |
检查点
1 | pwd |
确认当前目录就是仓库根目录,并能看到 Dockerfile 和 compose.yaml。
Step 2:理解 Dockerfile——它负责“镜像怎么做出来”
打开:
1 | Dockerfile |
第一行是:
1 | FROM ros:noetic-ros-base-focal |
它告诉 Docker:不要从一个空 Linux 文件系统开始,而是以官方 ROS1 Noetic ros-base Focal Image 为基础。
这个基础 Image 已经提供:
- Ubuntu 20.04 Focal userspace;
/opt/ros/noetic;- ROS1 基础命令;
/ros_entrypoint.sh;ROS_DISTRO=noetic等基础环境。
接下来 Dockerfile 安装:
1 | build-essential |
这些是“开发镜像”需要的工具,而不是每次进入 Container 后再手工安装。
为什么把工具写进 Dockerfile
如果你进入 Container 后手工执行:
1 | sudo apt install gdb |
然后未来重新 build Image,这个手工改动不会自动成为 Dockerfile 的一部分。
而写进 Dockerfile:
1 | RUN apt-get update && apt-get install ... |
意味着:
1 | Dockerfile |
每次从 Dockerfile 重建,都能得到同一组开发工具。
Step 3:为什么创建 dev 用户,而不是一直使用 root
Docker Container 默认很容易以 root 运行。如果整个仓库 bind mount 进去后,root 在 Container 中创建:
1 | ros_ws/build/ |
Host 上可能看到:
1 | root root |
这会让普通 Host 用户后续删除、编辑或 Git 操作变麻烦。
本工程通过 build args 把 Host UID/GID 传进 Dockerfile:
1 | args: |
Dockerfile 再创建:
1 | dev |
用户。
在 Host 生成 .env
执行:
1 | printf 'LOCAL_UID=%s\nLOCAL_GID=%s\n' \ |
查看:
1 | cat .env |
典型结果:
1 | LOCAL_UID=1000 |
检查 Compose 是否读到了变量
1 | docker compose config --environment |
重点确认 LOCAL_UID、LOCAL_GID 有值。
Step 4:理解 compose.yaml——它负责“Container 怎么运行”
Dockerfile 与 Compose 不要混为一谈。
1 | Dockerfile |
本工程只有一个服务:
1 | services: |
所以日常命令都会围绕:
1 | ros1-dev |
展开。
image 与 container_name
1 | image: ros1-noetic-dev:local |
含义分别是:
1 | Image 名: ros1-noetic-dev:local |
Image 更像“模板”;Container 是这个模板的一次运行实例。
Step 5:理解 bind mount——为什么源码不放进 Image
Compose 中:
1 | - type: bind |
表示:
1 | Host 仓库根目录 |
因此你在 Host 修改:
1 | ros_ws/src/ros1_hello/src/hello_node.cpp |
Container 中立刻看到:
1 | /workspace/ros_ws/src/ros1_hello/src/hello_node.cpp |
不需要重新 build Image。
为什么源码留在 Host
这样可以同时满足:
- Host Git 正常工作;
- Remote-SSH VS Code 直接编辑 Host 文件;
- Container 使用同一份源码编译;
- 删除 Container 不会删除源码;
- Image 只负责工具链,不承载你的工作区历史。
验证 bind mount
先启动 Container 后,在 Host:
1 | touch /home/wdfk/share/ros1-docker/.bind_mount_test |
Container 中:
1 | ls -l /workspace/.bind_mount_test |
能看到同一个文件就说明 bind mount 正常。
验证完删除:
1 | rm /workspace/.bind_mount_test |
Step 6:构建并启动开发 Container
Host 执行:
1 | docker compose up -d --build |
这里有两个动作:
1 | --build |
查看状态:
1 | docker compose ps |
应该能看到:
1 | ros1-dev ... Up |
如果 docker compose up 不加 -d,终端会一直占着。这不是卡死,而是 Compose 前台附着到了长期运行的服务。
Step 7:理解 sleep infinity——为什么 Container 不立刻退出
Container 的生命周期与主进程有关。
如果主进程结束:
1 | PID 1 退出 |
本工程在 Compose 中设置:
1 | command: sleep infinity |
它的意思就是:
1 | 一直等待,不主动退出 |
因此开发 Container 可以保持 Up,你再通过:
1 | docker compose exec ros1-dev bash |
进入它执行 catkin build、roscore、rosrun、gdb。
这种模式特别适合“长期开发环境 Container”,因为 ROS 进程不会被隐藏在 Compose 的启动命令里。
Step 8:理解 init: true
Compose 中还有:
1 | init: true |
它会在 Container 内放一个轻量 init 进程作为 PID 1。
主要职责:
- 转发
SIGINT、SIGTERM等信号; - 回收已经退出但父进程没有
wait()的僵尸子进程。
这对 roslaunch 很有意义,因为 roslaunch 可能启动多个子进程。
要特别注意:
1 | init: true |
不是:
1 | 启动 systemd |
也不是:
1 | 自动启动 roscore |
ROS 进程仍然由你自己手工运行。
Step 9:为什么学习阶段使用 network_mode: host
ROS1 的 Master、Node XML-RPC URI、TCPROS 连接都涉及网络地址。
如果一开始就使用 Docker bridge/NAT,还需要同时理解:
1 | Container IP |
会增加很多与 ROS 基础无关的变量。
本工程先使用:
1 | network_mode: host |
让 Container 与 Host 使用同一网络 namespace,简化单机学习。
这不是说所有生产 ROS1 系统都应该使用 Host Network,而是学习阶段先减少变量。
Step 10:GDB 为什么需要 SYS_PTRACE
Compose 中:
1 | cap_add: |
Linux 调试器会用 ptrace 一类机制观察/控制被调试进程。
Docker 默认限制很多能力,因此调试和 attach 场景可能需要:
1 | CAP_SYS_PTRACE |
本工程没有使用:
1 | privileged: true |
因为它的权限范围过大。
同时:
1 | security_opt: |
用于减少不同 Docker/Kernel 组合下 GDB syscall 过滤造成的兼容性问题。
它属于开发便利配置,会降低隔离强度,不应该不加判断地搬到生产环境。
Step 11:进入 Container,确认 ROS Noetic 环境
Host:
1 | docker compose exec ros1-dev bash |
Container:
1 | whoami |
预期类似:
1 | dev |
再检查:
1 | ls -ld /workspace |
这时要建立一个很重要的概念:
1 | /workspace |
两者来源完全不同。
Step 12:理解 /ros_entrypoint.sh
在 Container 中:
1 | cat /ros_entrypoint.sh |
核心通常类似:
1 | source "/opt/ros/$ROS_DISTRO/setup.bash" -- |
第一句让 ROS 环境进入当前 shell/process 环境,例如:
1 | ROS_DISTRO |
第二句:
1 | exec "$@" |
表示用真正要运行的命令替换 shell 进程。
本工程交互式 bash 又会 source:
1 | /workspace/.devcontainer/ros_env.bash |
它按照:
1 | /opt/ros/noetic |
形成 overlay 环境。
Step 13:把日常 Docker 命令练熟
先不要依赖插件按钮。建议至少能手工写出:
1 | # 后台启动 |
理解 stop 和 down 的区别:
1 | stop |
但 bind mount 的 Host 仓库不会因此被删除。
本章常见问题
现象 1:docker compose up 一直不返回
如果没有 -d,Compose 在前台运行是正常的。
改用:
1 | docker compose up -d |
再通过:
1 | docker compose ps |
确认状态。
现象 2:Host 出现 root:root 文件
先确认 .env:
1 | cat .env |
如果改变 UID/GID 后需要重新创建 Image:
1 | docker compose down |
现象 3:Container 一启动就退出
先看:
1 | docker compose ps -a |
然后检查 Compose 中是否仍有:
1 | command: sleep infinity |
现象 4:Container 中看不到 Host 文件
检查:
1 | docker inspect ros1-dev |
重点看 Mounts,确认 Host 仓库映射到 /workspace。
本章练习
不看本文,尝试回答:
- 删除 Container 后,
ros_ws/src为什么还在? - Dockerfile 和
compose.yaml各自负责什么? dst=/workspace或target: /workspace的含义是什么?sleep infinity为什么适合开发 Container?init: true和 systemd 有什么区别?- 为什么本工程不直接使用
privileged: true?
如果这些问题能自己解释,继续下一章。
下一章会从:
1 | /workspace/ros_ws |
开始,创建 catkin workspace、ROS package 和第一个 C++ Node。










