Docker使用教程
前言
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 | cat /etc/os-release |
下面的方式适用于 Docker 官方当前支持的 64 位 Ubuntu 版本,amd64 和 arm64 都可以使用。支持的 Ubuntu 版本可能会随时间变化,安装前可以在 Docker 官方 Ubuntu 安装文档 中确认。其他 Linux 发行版请参考 Docker Engine 安装索引。
1. 移除可能冲突的软件包
这一步只需要处理系统中已经安装的冲突软件包。下面的命令会先筛选出实际已安装的软件包:如果存在就交给 APT 卸载,如果不存在则只输出提示,不会出现一串“没有找到相匹配的软件包”的信息。
1 | packages_to_remove="$( |
这一步只移除可能与 Docker 官方软件包冲突的程序,不会自动删除 /var/lib/docker 里的已有镜像、容器和卷。如果这台机器已经在使用其他容器环境,先确认这些软件包没有被其他项目依赖,再执行卸载。
2. 添加 Docker 官方 APT 仓库
先安装下载和证书工具,并添加 Docker 的 GPG 密钥:
1 | sudo apt update |
再添加与当前 Ubuntu 版本及处理器架构匹配的软件源:
1 | sudo tee /etc/apt/sources.list.d/docker.sources > /dev/null <<EOF |
3. 安装并验证 Docker
安装 Docker Engine、命令行工具、containerd、Buildx 和 Compose 插件:
1 | sudo apt install -y \ |
启动 Docker,并设置为开机自动启动:
1 | sudo systemctl enable --now docker |
先查看 Docker Engine 和 Compose 的版本:
1 | sudo docker 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 | sudo nvidia-ctk runtime configure --runtime=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 | docker run -it \ |
这条命令会创建并进入一个名为 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 | docker run -it \ |
这里有几个需要注意的点:
--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 | mkdir -p /root/catkin_ws/src |
2. 安装依赖并编译
Diff-Planner 项目本身就是一个 catkin 工作空间,因此这里直接进入项目目录编译:
1 | cd /root/catkin_ws/src/Diff-Planner |
3. 启动测试
1 | cd /root/catkin_ws/src/Diff-Planner |
如果还需要在另一个终端里触发脚本,可以新开一个终端执行:
1 | docker exec -it ros_noetic bash |
六、挂载 /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 | docker run -it \ |
如果你之后需要补 --device、修改挂载目录,或者调整其他启动参数,比较常见的做法就是:
1 | docker commit ros_noetic ros_noetic_saved:latest |
然后再用新的 docker run 命令重新创建。
docker commit 的优点是快,适合先把当前能跑通的环境存下来;缺点是步骤不可追踪,后面时间长了不容易回忆当初到底改过什么。
2. 用 Dockerfile 固化环境
如果这个环境后续要长期维护,或者以后要迁移到别的机器,更推荐把安装步骤写成 Dockerfile。
例如:
1 | FROM osrf/ros:noetic-desktop-full |
然后执行:
1 | docker build -t ros_noetic_custom:latest . |
以后直接基于这个自定义镜像启动容器即可:
1 | docker run -it \ |
Dockerfile 的优点是可复现、可维护、方便迁移;缺点是第一次整理会比 docker commit 麻烦一些。
3. 该怎么选
- 只是临时保存当前能跑通的环境:优先用
docker commit - 想长期维护,或者准备换机器复现:更推荐写
Dockerfile - 比较实用的做法是:先
docker commit兜底,再有空把环境整理成Dockerfile
如果后面还想把镜像拷到别的机器,也可以再配合使用:
1 | docker save -o ros_noetic_saved.tar ros_noetic_saved:latest |
八、常见问题
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 | dial tcp <IP地址>:443: i/o timeout |
说明 Docker Engine 本身已经启动,问题发生在访问 Docker Hub 的过程中。
先检查宿主机能否直连 Docker Hub
执行:
1 | curl -I --connect-timeout 10 https://registry-1.docker.io/v2/ |
只要能够收到 HTTP 响应,就说明对应地址可达。其中 registry-1.docker.io 返回 401 Unauthorized 是正常现象,表示已经连到 Registry,只是当前请求没有携带镜像仓库的认证令牌;auth.docker.io 根路径返回 404 也不代表网络失败。
如果两条命令都连接超时,可以先更换网络或使用手机热点复测。如果当前网络必须通过代理访问外网,则需要为 Docker daemon 单独配置代理。Docker daemon 是系统服务,不会自动继承浏览器、桌面环境或当前终端里的代理设置。
为 Docker daemon 配置 HTTP 代理
先在代理软件中确认 HTTP 或 Mixed 代理端口。下面以本机端口 7890 为例,实际使用时必须换成代理软件显示的端口:
1 | PROXY_URL=http://127.0.0.1:7890 |
如果通过代理能够收到 401 Unauthorized,说明这条代理链路可以访问 Docker Registry。接着执行:
1 | sudo systemctl edit docker |
在打开的编辑器顶部非注释区域填入以下内容,并把两个 7890 都替换为实际端口:
1 | [Service] |
对于 Clash 这类 HTTP / Mixed 代理,即使目标网站使用 HTTPS,HTTPS_PROXY 的代理地址通常仍写成 http://127.0.0.1:<端口>,由 HTTP CONNECT 建立加密连接。不要把 SOCKS 端口误填到这个示例里。
保存退出后,重新加载 systemd 配置并重启 Docker:
1 | sudo systemctl daemon-reload |
确认输出中已经包含 HTTP_PROXY 和 HTTPS_PROXY,再测试:
1 | sudo docker pull hello-world |
这种配置依赖本机代理软件持续运行。如果代理软件退出或端口发生变化,Docker Hub 拉取会再次失败。完整配置说明可以参考 Docker 官方 daemon 代理文档。如果没有可用代理,可以使用学校、单位或自己维护的可信 Registry Mirror,不建议随意添加来源不明的公共镜像站。
代理可用但仍然出现 EOF
有时 Registry 首页通过代理可以访问,但 Docker 在请求镜像清单时仍然出现 EOF。可以分别测试默认协议和 HTTP/1.1:
1 | PROXY_URL=http://127.0.0.1:7890 |
如果第一条稳定出现 unexpected EOF,而第二条立即返回正常的 401 Unauthorized,说明当前代理链路的 HTTP/2 通信存在兼容问题。可以先尝试更新代理软件或更换代理节点;如果问题仍然存在,再执行:
1 | sudo systemctl edit docker |
在原有代理配置中增加 GODEBUG,完整内容类似下面这样:
1 | [Service] |
然后重新加载并验证:
1 | sudo systemctl daemon-reload |
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 | -e DISPLAY=$DISPLAY \ |
这种方式会把宿主机当前用户的 .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 之类的开发环境会顺很多。


