前言

Docker 是日常开发里很常用的容器工具,可以把程序和运行环境打包在一起,减少“换台机器就跑不起来”的问题。

这篇文章先整理 Docker 的基本概念和常用命令,再给出一个 ROS Noetic 图形化容器的实际示例,方便后续继续搭建自己的仿真环境。

一、先理解镜像和容器

可以先把两者理解成下面这个关系:

1
镜像 -> 创建 -> 容器
  • 镜像(Image):一个静态模板,里面包含程序代码、依赖库和运行环境。
  • 容器(Container):由镜像创建出来的运行实例,可以直接启动和使用。

例如,下面这条命令会基于 ubuntu 镜像启动一个 Ubuntu 容器,并进入容器终端:

1
docker run -it ubuntu bash

一个镜像可以创建多个容器,而多个容器之间彼此独立。

二、常用命令速查

功能 命令
查看 Docker 状态 docker info
查看本地镜像 docker images
拉取镜像 docker pull <镜像名>
查看正在运行的容器 docker ps
查看所有容器 docker ps -a
查看容器日志 docker logs <容器名或容器ID>
停止容器 docker stop <容器名或容器ID>
启动并进入容器 docker start -ai <容器名或容器ID>
进入正在运行的容器 docker exec -it <容器名或容器ID> bash
删除容器 docker rm <容器名或容器ID>
删除镜像 docker rmi <镜像名或镜像ID>

下面把这些命令按使用场景简单展开一下。

1. 查看 Docker 是否正常工作

1
docker info

这个命令可以查看 Docker 的整体运行状态,比如版本、容器数量、镜像数量、存储目录和系统信息。

如果执行时提示无法连接 Docker daemon,可以先检查服务状态:

1
sudo systemctl status docker

如果 Docker 服务没有启动,再执行:

1
sudo systemctl start docker

2. 查看本地镜像

1
docker images

用于查看本机已经下载或构建好的镜像。

3. 拉取镜像

1
docker pull osrf/ros:noetic-desktop-full

这条命令会下载 ROS Noetic 的桌面完整版镜像,后面运行 ROS 图形程序时会直接用到。

如果是在 Jetson 这类 ARM64 设备上,只需要 ROS Noetic 的基础运行环境,可以拉取更轻量的 ros-base 镜像:

1
docker pull --platform linux/arm64/v8 ros:noetic-ros-base-focal

在 Jetson 本机上执行时,Docker 通常会自动选择 ARM64 镜像;这里加上 --platform linux/arm64/v8 是为了把架构写得更明确。需要注意的是,ros-base 不包含 RViz、Gazebo 这类桌面图形工具,后面如果要运行图形程序,仍然需要安装对应软件包或使用桌面版镜像。

4. 查看容器

查看正在运行的容器:

1
docker ps

查看所有容器(包括已经停止的容器):

1
docker ps -a

如果容器状态是 Exited,通常说明容器已经退出,这时可以结合日志排查:

1
docker logs <容器名或容器ID>

5. 停止、重启和进入容器

停止容器:

1
docker stop <容器名或容器ID>

容器停止后,重新启动并直接进入:

1
docker start -ai <容器名或容器ID>

如果容器已经在运行,想在新的终端里再进入一次:

1
docker exec -it <容器名或容器ID> bash

6. 删除容器和镜像

删除已经停止的容器:

1
docker rm <容器名或容器ID>

删除本地镜像:

1
docker rmi <镜像名或镜像ID>

如果镜像仍被某个容器占用,需要先删除对应容器。

三、创建一个 ROS Noetic 图形化容器

这一节给一个比较常见的使用方式:在 Docker 里运行 ROS Noetic,同时把宿主机的图形界面和整个 /root 目录挂载进去。

1. 允许容器访问宿主机图形界面

1
xhost +SI:localuser:root

这一步的作用是允许本机 root 用户访问当前图形显示服务,容器里的 RViz、rqt、Gazebo 等图形程序才能在宿主机屏幕上显示出来。

相比 xhost +local:root,这里更推荐 xhost +SI:localuser:root,因为它只放行本机的 root 用户,范围更收敛一些。很多 ROS / Gazebo 镜像默认就是以 root 身份运行,所以通常够用。

2. 创建并进入容器

在执行 docker run 之前,建议先在宿主机上创建挂载目录:

1
mkdir -p ~/docker/ros_root

这样可以避免路径写错时被 Docker 悄悄新建成空目录,也更方便你确认权限和目录位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
docker run -it \
--name ros_noetic \
--network host \
--ipc host \
--gpus all \
-e DISPLAY=$DISPLAY \
-e XAUTHORITY=/root/.Xauthority \
-e QT_X11_NO_MITSHM=1 \
-e NVIDIA_DRIVER_CAPABILITIES=all \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v $HOME/.Xauthority:/root/.Xauthority:ro \
-v ~/docker/ros_root:/root \
osrf/ros:noetic-desktop-full \
bash

这条命令会创建并进入一个名为 ros_noetic 的容器。几个关键参数的作用如下:

  • --network host:共享宿主机网络,ROS 通信更省事。
  • --ipc host:共享 IPC,某些图形和仿真程序更稳定。
  • --gpus all:把 GPU 暴露给容器;如果机器没有 NVIDIA 环境,可以先去掉这一项。
  • -e DISPLAY=$DISPLAY-v /tmp/.X11-unix:/tmp/.X11-unix:让容器里的图形程序能显示到宿主机。
  • -e XAUTHORITY=/root/.Xauthority-v $HOME/.Xauthority:/root/.Xauthority:ro:把宿主机当前图形会话的 X11 认证信息挂进容器,很多情况下可以减少手动执行 xhost 的需要。
  • -v ~/docker/ros_root:/root:把容器内整个 /root 目录挂载到宿主机,代码、配置和 home 目录下的其他文件都会一起保存。

如果后续需要让容器里的程序访问宿主机串口或 USB 设备,例如 /dev/ttyACM0/dev/ttyUSB0/dev/ttyTHS1,通常要在创建容器时就加上 --device 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker run -it \
--name ros_noetic \
--network host \
--ipc host \
--runtime=nvidia \
--gpus all \
--device=/dev/ttyACM0 \
--device=/dev/ttyTHS1 \
-e DISPLAY=$DISPLAY \
-e XAUTHORITY=/root/.Xauthority \
-e QT_X11_NO_MITSHM=1 \
-e NVIDIA_DRIVER_CAPABILITIES=all \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v $HOME/.Xauthority:/root/.Xauthority:ro \
-v ~/docker/ros_root:/root \
osrf/ros:noetic-desktop-full \
bash

这里有几个需要注意的点:

  • --network host 和目录挂载并不会自动让容器获得串口访问权限,设备需要单独通过 --device 暴露进去。
  • --device=/dev/ttyACM0 这种写法要求宿主机上当前确实存在这个设备,否则容器创建时会报错。
  • 如果你创建容器时没有指定 --device,后续不能通过 docker startdocker exec 再补上,一般需要删除旧容器后重新创建。
  • 如果只是不确定设备号,可以先在宿主机执行 ls /dev/ttyUSB* /dev/ttyACM* /dev/ttyTHS* 确认实际存在的设备,再决定要不要加到启动命令里。

退出容器时直接执行:

1
exit

3. 后续重新进入容器

容器停止后,重新启动并进入:

1
docker start -ai ros_noetic

如果容器已经在运行,另开一个终端进入:

1
docker exec -it ros_noetic bash

四、在容器里编译 Diff-Planner

下面以 Diff-Planner 为例,演示如何在容器里编译一个 ROS1 工程。

依赖安装
apt update
apt-get install git

1. 进入源码目录并下载项目

1
2
3
mkdir -p /root/catkin_ws/src
cd /root/catkin_ws/src
git clone https://github.com/DifferentialRobotics/Diff-Planner.git

2. 安装依赖并编译

Diff-Planner 项目本身就是一个 catkin 工作空间,因此这里直接进入项目目录编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cd /root/catkin_ws/src/Diff-Planner

apt update
apt install -y \
python3-catkin-tools \
ros-noetic-cmake-modules \
ros-noetic-pcl-ros \
ros-noetic-pcl-conversions \
ros-noetic-mavros \
ros-noetic-mavros-msgs \
libarmadillo-dev \
libeigen3-dev \
libpcl-dev \
qtbase5-dev \
zsh

source /opt/ros/noetic/setup.bash
catkin_make

3. 启动测试

1
2
3
cd /root/catkin_ws/src/Diff-Planner
source devel/setup.bash
roslaunch diff_planner run_sim_swarm.launch

如果还需要在另一个终端里触发脚本,可以新开一个终端执行:

1
2
3
4
docker exec -it ros_noetic bash
cd /root/catkin_ws/src/Diff-Planner
source devel/setup.bash
./sh_files/pub_swarm_trigger.sh

五、挂载 /root 的作用

前面这条挂载:

1
-v ~/docker/ros_root:/root

表示宿主机目录 ~/docker/ros_root 和容器目录 /root 是同步的。

也就是说:

  • 你在宿主机里修改 ~/docker/ros_root 的内容,容器里会同步变化。
  • 你在容器里放在 /root 下的代码、配置、日志和脚本,宿主机里也能直接看到。

这样即使容器删掉了,/root 目录下的数据依然保存在宿主机,不容易丢。

不过要注意,挂载 /root 只能保存用户目录里的内容,不能代替整个容器系统本身。比如通过 apt install 安装到 /usr/etc/var 下的系统级依赖,仍然属于容器环境的一部分。

六、如何保存容器环境

前面挂载 /root 可以保存代码、配置和日志,但如果你在容器里额外安装了软件包、改了系统环境,单纯保留 /root 还不够。这时候通常有两种方式保存容器环境。

1. 用 docker commit 保存当前容器

如果你已经在容器里手动装好了依赖,想先把当前状态直接保存下来,可以执行:

1
docker commit ros_noetic ros_noetic_saved:latest

这条命令会把当前容器 ros_noetic 保存成一个新的镜像 ros_noetic_saved:latest

这里要注意:docker commit 保存的是容器当前文件系统状态,但像 -v ~/docker/ros_root:/root 这种挂载到宿主机的目录,本来就不属于镜像本体,所以不会被重新打包进镜像里。不过这部分数据已经在宿主机上,一般也不需要靠 commit 再保存一次。

之后就可以基于这个镜像重新创建容器:

1
2
3
4
5
6
7
8
9
10
11
12
docker run -it \
--name ros_noetic \
--network host \
--ipc host \
--gpus all \
-e DISPLAY=$DISPLAY \
-e QT_X11_NO_MITSHM=1 \
-e NVIDIA_DRIVER_CAPABILITIES=all \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v ~/docker/ros_root:/root \
ros_noetic_saved:latest \
bash

如果你之后需要补 --device、修改挂载目录,或者调整其他启动参数,比较常见的做法就是:

1
2
3
docker commit ros_noetic ros_noetic_saved:latest
docker stop ros_noetic
docker rm ros_noetic

然后再用新的 docker run 命令重新创建。

docker commit 的优点是快,适合先把当前能跑通的环境存下来;缺点是步骤不可追踪,后面时间长了不容易回忆当初到底改过什么。

2. 用 Dockerfile 固化环境

如果这个环境后续要长期维护,或者以后要迁移到别的机器,更推荐把安装步骤写成 Dockerfile

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM osrf/ros:noetic-desktop-full

RUN apt update && apt install -y \
python3-catkin-tools \
ros-noetic-cmake-modules \
ros-noetic-pcl-ros \
ros-noetic-pcl-conversions \
ros-noetic-mavros \
ros-noetic-mavros-msgs \
libarmadillo-dev \
libeigen3-dev \
libpcl-dev \
qtbase5-dev \
zsh

然后执行:

1
docker build -t ros_noetic_custom:latest .

以后直接基于这个自定义镜像启动容器即可:

1
2
3
4
5
6
7
8
9
10
11
12
docker run -it \
--name ros_noetic \
--network host \
--ipc host \
--gpus all \
-e DISPLAY=$DISPLAY \
-e QT_X11_NO_MITSHM=1 \
-e NVIDIA_DRIVER_CAPABILITIES=all \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v ~/docker/ros_root:/root \
ros_noetic_custom:latest \
bash

Dockerfile 的优点是可复现、可维护、方便迁移;缺点是第一次整理会比 docker commit 麻烦一些。

3. 该怎么选

  • 只是临时保存当前能跑通的环境:优先用 docker commit
  • 想长期维护,或者准备换机器复现:更推荐写 Dockerfile
  • 比较实用的做法是:先 docker commit 兜底,再有空把环境整理成 Dockerfile

如果后面还想把镜像拷到别的机器,也可以再配合使用:

1
2
docker save -o ros_noetic_saved.tar ros_noetic_saved:latest
docker load -i ros_noetic_saved.tar

七、常见问题

1. 无法连接 Docker daemon

先检查 Docker 服务:

1
sudo systemctl status docker

如未启动:

1
sudo systemctl start docker

2. Gazebo 启动时报图形权限错误

如果宿主机重启后,在容器里运行 gazebo 出现下面这个报错:

1
Authorization required, but no authorization protocol specified

说明容器当前没有权限连接宿主机显示器。重新执行一次:

1
xhost +SI:localuser:root

通常在宿主机重启、注销重登,或者 X / 图形会话重启后,都需要再执行一遍。因为 xhost 授权是给当前图形会话的,不是永久系统配置。

如果你希望每次登录图形界面后自动执行,可以把下面这行加入宿主机的登录启动脚本,例如 ~/.profile,或者桌面环境的“启动应用程序”里:

1
xhost +SI:localuser:root

这样做的效果更接近“半永久配置”:不是系统级永久放行,而是每次图形会话启动后自动重新授权。

如果想再干净一点,可以在创建容器时正确挂载 X11 认证文件:

1
2
3
4
-e DISPLAY=$DISPLAY \
-e XAUTHORITY=/root/.Xauthority \
-v /tmp/.X11-unix:/tmp/.X11-unix:rw \
-v $HOME/.Xauthority:/root/.Xauthority:ro

这种方式会把宿主机当前用户的 .Xauthority 挂到容器内,很多情况下即使不手动执行 xhost,容器里的 Gazebo、RViz 也能直接连接显示服务。

不过在 ROS / Gazebo 的 Docker 环境里,最省事的做法通常还是:宿主机每次开机并进入桌面后,先执行一次:

1
xhost +SI:localuser:root

再进入容器启动 Gazebo 或 RViz。

3. 没有 NVIDIA 显卡怎么办

如果你的机器没有配置 NVIDIA 驱动或 Docker GPU 环境,创建容器时先去掉:

1
--gpus all

以及:

1
-e NVIDIA_DRIVER_CAPABILITIES=all

总结

Docker 的核心思路并不复杂:先有镜像,再由镜像创建容器。真正高频使用的内容,其实就是拉镜像、启容器、进容器、挂目录和排查图形权限这几件事。

把这些基础操作熟悉之后,再继续搭 ROS、Gazebo、PX4 之类的开发环境会顺很多。