前言

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

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

一、先理解镜像和容器

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

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

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

1
docker run -it ubuntu bash

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

二、在 Ubuntu 安装 Docker

从下一章开始,本文默认宿主机已经安装 Docker Engine。下面以 Ubuntu 为例,通过 Docker 官方 APT 仓库安装最新版 Docker Engine,同时安装 Buildx 和 Docker Compose 插件。

开始前可以先确认系统版本和处理器架构:

1
2
cat /etc/os-release
dpkg --print-architecture

下面的方式适用于 Docker 官方当前支持的 64 位 Ubuntu 版本,amd64 和 arm64 都可以使用。支持的 Ubuntu 版本可能会随时间变化,安装前可以在 Docker 官方 Ubuntu 安装文档 中确认。其他 Linux 发行版请参考 Docker Engine 安装索引。

1. 移除可能冲突的软件包

这一步只需要处理系统中已经安装的冲突软件包。下面的命令会先筛选出实际已安装的软件包:如果存在就交给 APT 卸载,如果不存在则只输出提示,不会出现一串“没有找到相匹配的软件包”的信息。

1
2
3
4
5
6
7
8
9
10
11
12
packages_to_remove="$(
dpkg-query -W -f='${binary:Package}\t${db:Status-Abbrev}\n' \
docker.io docker-compose docker-compose-v2 docker-doc \
docker-buildx podman-docker containerd runc 2>/dev/null | \
awk '$2 == "ii" { print $1 }'
)"

if [ -n "$packages_to_remove" ]; then
sudo apt remove $packages_to_remove
else
echo "未检测到冲突软件包,可以继续安装 Docker。"
fi

这一步只移除可能与 Docker 官方软件包冲突的程序,不会自动删除 /var/lib/docker 里的已有镜像、容器和卷。如果这台机器已经在使用其他容器环境,先确认这些软件包没有被其他项目依赖,再执行卸载。

2. 添加 Docker 官方 APT 仓库

先安装下载和证书工具,并添加 Docker 的 GPG 密钥:

1
2
3
4
5
6
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

再添加与当前 Ubuntu 版本及处理器架构匹配的软件源:

1
2
3
4
5
6
7
8
9
10
sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update

3. 安装并验证 Docker

安装 Docker Engine、命令行工具、containerd、Buildx 和 Compose 插件:

1
2
3
4
5
6
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin

启动 Docker,并设置为开机自动启动:

1
2
sudo systemctl enable --now docker
sudo systemctl status docker --no-pager

先查看 Docker Engine 和 Compose 的版本:

1
2
sudo docker version
docker compose version

docker version 同时显示 Client 和 Server 信息,并且前面的 Docker 服务状态为 active,就说明 Docker Engine 已经安装并启动成功。docker compose version 能输出版本号,则说明 Compose 插件也已安装。

接下来运行官方测试容器,额外检查 Docker Hub 网络、镜像拉取和容器运行是否都正常:

1
sudo docker run --rm hello-world

第一次执行时出现下面这行是正常现象,它只表示本地还没有这个镜像,Docker 正准备从 Docker Hub 下载:

1
Unable to find image 'hello-world:latest' locally

终端最终出现 Hello from Docker!,说明 Docker 已经可以正常拉取并运行容器。如果随后出现 i/o timeout、EOF 或连接超时,而 docker version 的 Client 和 Server 信息正常,通常是 Docker Hub 网络或代理问题,并不代表 Docker 安装失败,可以参考后文“拉取镜像卡住、超时或出现 EOF”。

4. 配置当前用户免 sudo 使用 Docker

后面的示例直接使用 docker 命令。为了不在每条命令前添加 sudo,可以把当前用户加入 docker 用户组:

1
sudo usermod -aG docker "$USER"

执行后注销并重新登录,让用户组权限生效;也可以在当前终端临时执行:

1
newgrp docker

然后再次验证:

1
docker run --rm hello-world

需要注意,docker 用户组能够控制 Docker daemon,实际拥有接近 root 的系统权限。只应把可信用户加入这个组;如果不希望授予这项权限,就保留默认配置,并在 Docker 命令前使用 sudo。更多说明可以参考 Docker 官方 Linux 安装后配置。

5. 使用 NVIDIA GPU 前的额外准备

安装 Docker Engine 并不会自动提供 GPU 容器支持。后文的 --gpus all 参数还要求宿主机已经正确安装 NVIDIA 驱动和 NVIDIA Container Toolkit。

先在宿主机执行 nvidia-smi,确认显卡驱动正常,再按照 NVIDIA Container Toolkit 官方安装文档 安装工具包。安装后配置 Docker runtime 并重启 Docker:

1
2
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

可以用 NVIDIA 官方示例检查容器是否能够访问显卡:

1
docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi

如果不需要使用 NVIDIA GPU,跳过这一小节,并在后面的 docker run 命令中去掉 --gpus all 和 NVIDIA 相关环境变量即可。

三、常用命令速查

功能 命令
查看 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 start 或 docker 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. 拉取镜像卡住、超时或出现 EOF

如果 docker version 能正常显示 Client 和 Server,但执行 docker pull 或 docker run 时长时间停在下面这行:

1
Unable to find image 'hello-world:latest' locally

或者最终出现类似报错:

1
2
dial tcp <IP地址>:443: i/o timeout
failed to do request: ... EOF

说明 Docker Engine 本身已经启动,问题发生在访问 Docker Hub 的过程中。

先检查宿主机能否直连 Docker Hub

执行:

1
2
curl -I --connect-timeout 10 https://registry-1.docker.io/v2/
curl -I --connect-timeout 10 https://auth.docker.io/

只要能够收到 HTTP 响应,就说明对应地址可达。其中 registry-1.docker.io 返回 401 Unauthorized 是正常现象,表示已经连到 Registry,只是当前请求没有携带镜像仓库的认证令牌;auth.docker.io 根路径返回 404 也不代表网络失败。

如果两条命令都连接超时,可以先更换网络或使用手机热点复测。如果当前网络必须通过代理访问外网,则需要为 Docker daemon 单独配置代理。Docker daemon 是系统服务,不会自动继承浏览器、桌面环境或当前终端里的代理设置。

为 Docker daemon 配置 HTTP 代理

先在代理软件中确认 HTTP 或 Mixed 代理端口。下面以本机端口 7890 为例,实际使用时必须换成代理软件显示的端口:

1
2
3
4
PROXY_URL=http://127.0.0.1:7890

curl -I -x "$PROXY_URL" --connect-timeout 10 \
https://registry-1.docker.io/v2/

如果通过代理能够收到 401 Unauthorized,说明这条代理链路可以访问 Docker Registry。接着执行:

1
sudo systemctl edit docker

在打开的编辑器顶部非注释区域填入以下内容,并把两个 7890 都替换为实际端口:

1
2
3
4
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,::1"

对于 Clash 这类 HTTP / Mixed 代理,即使目标网站使用 HTTPS,HTTPS_PROXY 的代理地址通常仍写成 http://127.0.0.1:<端口>,由 HTTP CONNECT 建立加密连接。不要把 SOCKS 端口误填到这个示例里。

保存退出后,重新加载 systemd 配置并重启 Docker:

1
2
3
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl show --property=Environment docker

确认输出中已经包含 HTTP_PROXY 和 HTTPS_PROXY,再测试:

1
2
sudo docker pull hello-world
sudo docker run --rm hello-world

这种配置依赖本机代理软件持续运行。如果代理软件退出或端口发生变化,Docker Hub 拉取会再次失败。完整配置说明可以参考 Docker 官方 daemon 代理文档。如果没有可用代理,可以使用学校、单位或自己维护的可信 Registry Mirror,不建议随意添加来源不明的公共镜像站。

代理可用但仍然出现 EOF

有时 Registry 首页通过代理可以访问,但 Docker 在请求镜像清单时仍然出现 EOF。可以分别测试默认协议和 HTTP/1.1:

1
2
3
4
5
PROXY_URL=http://127.0.0.1:7890
MANIFEST_URL=https://registry-1.docker.io/v2/library/hello-world/manifests/latest

curl -I -x "$PROXY_URL" --connect-timeout 10 "$MANIFEST_URL"
curl --http1.1 -I -x "$PROXY_URL" --connect-timeout 10 "$MANIFEST_URL"

如果第一条稳定出现 unexpected EOF,而第二条立即返回正常的 401 Unauthorized,说明当前代理链路的 HTTP/2 通信存在兼容问题。可以先尝试更新代理软件或更换代理节点;如果问题仍然存在,再执行:

1
sudo systemctl edit docker

在原有代理配置中增加 GODEBUG,完整内容类似下面这样:

1
2
3
4
5
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,::1"
Environment="GODEBUG=http2client=0"

然后重新加载并验证:

1
2
3
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo docker pull hello-world

GODEBUG=http2client=0 会让 Docker daemon 使用的 Go HTTP 客户端禁用 HTTP/2,回退到 HTTP/1.1。它是针对上述特定兼容问题的处理方式,不应作为所有电脑的默认配置。相关机制可以参考 Go 官方 GODEBUG 文档。

如果还无法判断具体失败位置,可以查看 Docker 最近的服务日志:

1
sudo journalctl -u docker --since "5 minutes ago" --no-pager

3. 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。

4. 没有 NVIDIA 显卡怎么办

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

1
--gpus all

以及:

1
-e NVIDIA_DRIVER_CAPABILITIES=all

总结

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

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