1. 为什么要纠结“系统里有几个 docker”:一次真实的环境冲突复盘
先说个我自己的经历。前两年接手一台 Ubuntu 20.04 服务器,前任运维留下的环境。我跑docker ps明明能列出容器,但systemctl restart docker之后容器全没了,后来发现/var/run/docker.sock指向的是 snap 版 docker 的 socket,而我以为我控制的 systemd 服务是 apt 版 docker。同一个系统里同时装了两套 docker 客户端、两套守护进程,互相打架。从那以后我养成了一个习惯:凡是拿到一台陌生的 Ubuntu 机器,第一件事就是先搞清楚这台机器上到底“有几个 docker”——这里说的不是有几个容器,而是有几套 Docker 相关的安装实体。
这句话听起来像是废话,但实际踩坑的人不在少数。你可能会遇到这些场景:
docker -v显示的版本和docker version显示的 Server 版本对不上,客户端是 24.x,服务端却是 20.x。- 明明执行了
systemctl stop docker,但docker ps依然能连上某个守护进程,容器照常运行。 - 想升级 docker,结果 apt 和 snap 两套包管理各装了一份,升级完一个另一个还是旧的。
- 排查问题时
whereis docker能看到多个可执行文件路径,但你不知道当前 shell 实际用的是哪个。
这篇文章我就以 Ubuntu(涵盖 20.04/22.04/24.04)为背景,手把手教你把系统里的 Docker 相关实体全部揪出来,搞清楚彼此之间的关系,并且给你一套可以直接复制运行的排查脚本。内容不追求面面俱到,重点放在“如何判断系统里有几套 Docker 安装、如何识别它们各自的状态、以及遇到冲突时怎么处理”。
2. 安装方式的差别决定了你会看到几个 Docker
2.1 Ubuntu 下 Docker 的三种主流安装路径
想要排查“有几个 docker”,先得知道 Ubuntu 上 Docker 可能的来源。很多冲突都源于不同来源的 Docker 同时存在,各自又互不知情。
| 安装方式 | 可执行文件位置 | 服务管理方式 | 套接字默认位置 |
|---|---|---|---|
| apt 官方源安装 | /usr/bin/docker | systemd(docker.service) | /var/run/docker.sock |
| Docker 官方 apt 源安装 | /usr/bin/docker | systemd(docker.service) | /var/run/docker.sock |
| snap 安装 | /snap/bin/docker(软链) | snap 服务(snap.docker.dockerd) | /var/run/docker.sock(也走 snap 的 socket) |
| 二进制手动安装 | 自定义目录(如 /usr/local/bin/docker) | 不一定有服务文件,可能用 screen / nohup 拉起 | 按启动参数而定 |
最常见也最坑的组合:系统里同时存在 apt 版和 snap 版。Ubuntu 的软件商店默认推荐 snap,而很多教程让你用 apt 装 docker.io 或官方源。如果你手滑两个都装过,就会出现上面说的那种“看起来能用、但深挖全是坑”的现状。
提示:判断当前 shell 实际调用的是哪个 docker,不要只看
which docker,因为 PATH 里哪个目录靠前哪个就生效。which只能告诉你当前环境调用的是哪一个,而不是系统里总共有几个。
2.2 为什么 apt 版和 snap 版会“同时存在”
apt 安装的包路径是/usr/bin/docker,snap 安装的路径则是/snap/bin/docker。两个可执行文件互不覆盖,因为 snap 有自己独立的挂载机制。所以从文件系统层面看,至少两套文件是同时真实存在的。再加上 snap 的 docker 还会自带的 dockerd 守护进程由 snap 服务拉起,而 apt 版同样注册了 systemd 的 docker.service。如果你不认真查,shell 走哪套全看环境变量里 PATH 的优先级——Ubuntu 默认 PATH 中/usr/bin排在/snap/bin之前,所以默认情况下你用的很可能是 apt 版。
这就是为什么很多人看到的现象是:docker -v显示一个版本,snap list docker又显示另一个版本,而systemctl status docker和snap services docker都在“active”。系统里确实是两个 docker。
3. 第一轮排查:用 docker 自身的命令确认客户端和服务端
3.1 docker version 比 docker -v 信息量大得多
很多新手只知道docker -v,这只能看到客户端版本。要看当前 shell 使用的 docker 是否真的连通了服务端,必须用docker version。它分为 Client 和 Server 两段输出。Server 段落能正常列出版本和 API 版本,说明客户端能找到并连上了一个守护进程;Server 段直接报错Cannot connect to the Docker daemon,则说明当前环境没有可用的服务端,或者 socket 路径不对。
$ docker version Client: Docker Engine - Community Version: 26.1.4 ... Server: Docker Engine - Community Engine: Version: 26.1.4关键点在于:docker version里的 Client 版本和 Server 版本可以不一致,因为客户端和服务端走的是 HTTP API 通信,只要 API 版本兼容就行。但大版本差太多时会报错提示server version is too old。如果你看到 Client 和 Server 版本明显不匹配,先别急着骂系统,很可能是因为你 shell 里调用的 docker 是 A 套,而正在监听 socket 的 dockerd 属于 B 套。
3.2 查看当前 docker 可执行文件真实路径
which docker type -a docker whereis dockerwhich docker:给出 PATH 中第一个命中的可执行文件路径。type -a docker:会把所有出现在 PATH 里的 docker 都列出来,这是 Shell 内建命令,比 which 更直观。whereis docker:会扫默认二进制目录,可以看到/usr/bin/docker、/snap/bin/docker等多个路径。
实测过程中最常见的结果是:which docker返回/usr/bin/docker,但type -a docker还能看到/snap/bin/docker。要知道,即使你不会主动输入/snap/bin/docker,如果你曾经在某个服务脚本里写了绝对路径调用它,那么那个进程用的就是 snap 版。这也是排查问题的突破口。
$ type -a docker docker is /usr/bin/docker docker is /snap/bin/docker3.3 用 docker context 看当前连的是哪个 socket
Docker 从 19.03 开始引入了 context 机制,可以配置多个 endpoint。默认 context 是default,连的是unix:///var/run/docker.sock。如果你之前手动创建过 context,或者 Docker Desktop 之类的工具改过配置,当前 shell 可能连的根本不是/var/run/docker.sock。
docker context ls输出会列出所有 context 以及当前激活的那个(带*号的)。查看详情:
docker context inspect这个命令能告诉你当前 context 的 Endpoint 到底是什么。我排障时经常先看一眼这里,确认是不是被某个自定义 context 带偏了方向。
提示:如果你在 Ubuntu 上装了 Docker Desktop(目前主要是基于 WSL2 或特定桌面版),它的 SDK 和 CLI 可能会单独管理一套上下文,和系统里的 docker.sock 不是一回事。涉及到 Docker Desktop 的场景,优先检查
~/.docker/config.json和docker context ls。
4. 第二轮排查:systemd、snap 服务与 socket 的三方对照
4.1 systemctl 能查到 systemd 注册的 docker 服务
如果 Docker 是 apt 安装的,通常会有 systemd 服务文件:
systemctl list-unit-files | grep -i docker systemctl status docker输出一般长这样:
● docker.service - Docker Application Container Engine Loaded: loaded (/lib/systemd/system/docker.service; enabled; preset: enabled) Active: active (running) since ...这会告诉你 systemd 视角下有一个 docker 守护进程正在运行。但注意:这只是“systemd 管理的那个”docker 实例。如果系统里还有 snap 版 docker,snap 版有自己独立的服务,不归 systemd 直接管,而是通过snap命令查看。
snap list | grep docker snap services dockersnap services docker的输出一般包含dockerd和containerd等。如果 snap 服务也是 active,那你基本可以确认系统里有两套服务同时存在。
4.2 对照 socket 文件找出真正的守护进程所有者
Docker 客户端默认通过/var/run/docker.sock和守护进程通信。如果安装方式不唯一,这个 socket 文件可能被不同的守护进程占用。排查方法:
ls -l /var/run/docker.sock如果输出显示 owner 是 root 且 socket 文件存在,一般来说有守护进程在监听。但你还得确认这个 socket 对应哪个进程。一条命令搞定:
sudo ss -xlp | grep docker它会列出 Unix socket 监听的进程。ss里的-x表示显示 Unix socket,-l表示监听状态,-p显示进程信息。输出会直接显示类似:
u_str LISTEN 0 4096 /var/run/docker.sock 12345 users:(("dockerd",pid=1234,fd=3))到这里你能发现一个关键事实:真正监听 docker.sock 的是哪个 dockerd 进程,这个进程的 PID 是什么,它由谁拉起。
4.3 进程层面:所有 dockerd 和 containerd 都列出来
不要只看一个 socket,直接按进程名找:
ps aux | grep -E "dockerd|containerd" | grep -v grep这个方法可以列出所有正在运行的守护进程相关进程。如果发现两个不同的 dockerd 进程,而且一个的开始命令里带snap字样,另一个是/usr/bin/dockerd,那系统里就是同时有两套 Docker 运行时在跑。两个进程如果都在监听 socket 但监听路径不同,那么客户端只能连上其中某一个,取决于你的DOCKER_HOST环境变量或 context 配置。
env | grep -i docker检查是否有DOCKER_HOST环境变量。一旦你设置了DOCKER_HOST=tcp://...,那么你本地所有 docker 命令都不会走本机 socket,而是去连远程地址。这种状态下你查到的“Server 版本”可能根本不是本机 dockerd。Ubuntu 桌面环境一般不会设置这个变量,但服务器环境里有些人会配。
5. 第三轮排查:把文件系统里所有 docker 实体挖出来
5.1 不同位置的 docker 二进制文件
我建议做一个全盘扫描,找出所有名称包含 docker 的可执行文件或者目录:
sudo find / -name "docker" -type f 2>/dev/null sudo find / -name "dockerd" -type f 2>/dev/null这个命令不要在小内存机器上跑全盘,但一般服务器也没多大负担。扫描结果里最常见的:
/usr/bin/docker:apt 安装/snap/bin/docker:snap 安装/usr/local/bin/docker:手动二进制安装/usr/libexec/docker/cli-plugins/:docker compose 等插件目录
如果你看到/usr/local/bin/docker,那说明曾经有人手动下过二进制包,这种安装方式最麻烦——它不会注册任何服务文件,没有 systemd 单元,也不会出现在 apt 或 snap 的包管理列表里,但如果你手动跑过dockerd,它照样可以运行并占用 socket。
5.2 配置目录和服务文件
/etc/docker/daemon.json:这是 dockerd 的核心配置。无论哪套安装,只要用了这个路径的配置文件,它都会生效。如果你看到/etc/docker/下有配置文件,但 systemd 服务是 disabled 状态,就要怀疑是不是有手动启动的 dockerd 进程在读取这个配置。/etc/systemd/system/docker.service.d/:apt/官方源安装时可能存在的 override 目录。/var/lib/docker/:默认的数据目录。如果你按数据目录大小判断,也可以看到是否真的有两个独立的数据目录,例如/var/lib/docker和/var/snap/docker/common/var-lib-docker。
snap 版 docker 默认数据目录在/var/snap/docker/common/var-lib-docker,和 apt 版的/var/lib/docker完全隔离。如果你发现两个目录都存在且都有大量数据,说明两套 docker 都实际工作过。这个信息很重要——如果你想卸载旧的那一套再迁移数据,就得弄清数据分别存哪。
5.3 检查包管理器的安装记录
用 apt 和 snap 分别查:
dpkg -l | grep -i docker snap list | grep -i dockerdpkg列表会展示 docker.io、docker-ce、docker-ce-cli、containerd.io 等包。注意docker.io是 Ubuntu 仓库自带的旧版,docker-ce是 Docker 官方源里的版本。如果两个同时存在,dpkg 会显示两个独立的包,但实际上docker-ce会替换/usr/bin/docker,而docker.io可能已经处于半卸载状态。查看包是否被接管:
dpkg -l | grep docker出现rc状态(已删除但配置文件还在)的包不算真正的安装,不用管。但若出现两个版本都是ii(installed),就说明你真的装了两套基于 systemd 的 docker。同理,snap list 里看到 docker 就说明还有一套 snap 的。
5.4 再补充一个:用户态 rootless docker
Ubuntu 上还可能存在 rootless 模式的 docker,这种模式不会用 systemd 服务,也不用/var/run/docker.sock,而是在用户目录下:
ls -l ~/.docker ls -l /run/user/$(id -u)/docker.sock如果你曾经用dockerd-rootless.sh启动过 rootless docker,那么系统里也会有一个 dockerd 进程,但它监听的 socket 路径是/run/user/1000/docker.sock之类。这时候你用默认的docker ps连的是系统级 docker,而那个 rootless docker 往往被忽略了。这也是“系统里有多个 docker”的一种隐藏形态。
6. 实际排查案例:一台 Ubuntu 22.04 上找出 3 套 Docker
为了让你更有体感,我把一次真实排查过程记录下来。机器是我的测试机,现象是重启后docker ps能列出容器,但systemctl restart docker再执行却提示“Unit docker.service not found”。这个现象本身就很典型:说明当前 shell 用的 docker 客户端不是 systemd 管理的那个。
第一步,确认系统里能看到的 docker 实体:
$ which docker /usr/local/bin/docker $ type -a docker docker is /usr/local/bin/docker docker is /usr/bin/docker docker is /snap/bin/docker这里已经出现三个 docker 可执行文件。/usr/local/bin在 PATH 中优先级最高,所以 shell 默认调用了手动安装的二进制。
第二步,查看当前客户端连的 Server:
$ docker version Client: 20.10.17 Server: 20.10.3Client 和 Server 都来自同一个二进制版本 20.10.17? 实际上 Client 显示 20.10.17 说明/usr/local/bin/docker是 20.10.17,Server 显示 20.10.3 说明它连接的 dockerd 是另一个版本,这个 dockerd 是 20.10.3,我推测是 snap 版或 apt 版。因为手动安装的二进制版本和系统里已有的 dockerd 不一定一致,两者之间只要 API 兼容就能工作。
第三步,检查 socket 监听进程:
$ sudo ss -xlp | grep docker u_str LISTEN 0 4096 /var/run/docker.sock 12345 users:(("dockerd",pid=9876,fd=3))查看 pid=9876 来自哪里:
$ ps -p 9876 -o command= /usr/bin/dockerd好,这个监听默认 socket 的是/usr/bin/dockerd,属于 apt 版本。
第四步,检查 snap 服务:
$ snap services docker Service Startup Current Notes docker.dockerd enabled active -说明 snap 版也在运行,但 snap 的 dockerd 没有占用/var/run/docker.sock。snap docker 默认也绑到/var/run/docker.sock?这里涉及 snap 接口的 socket 连接,但实际通过ss只看到一个监听者,说明 snap 的 dockerd 监听的是自己的 socket 或者处于失败重试状态。进一步查:
$ ps aux | grep dockerd | grep -v grep root 9876 ... /usr/bin/dockerd root 5432 ... /snap/docker/current/bin/dockerd最终结论:这台机器有 3 个 dockerd 进程?不,第二行ps只显示了一个/usr/bin/dockerd和一个/snap/docker/current/bin/dockerd。snap 的这个进程起来了吗?如果两个 dockerd 都监听同一个 socket 是不可能的,第二次绑定会失败。具体哪个是 slave?可以看进程状态,一个处于 active 状态监听 socket,另一个可能一直重试绑定 socket 或监听其他路径。
经过sudo lsns查看命名空间,发现 snap dockerd 被运行在一个独立的 mount 和 net namespace 中。这就是 snap 包特有的安全隔离:snap docker 的 dockerd 实际上跑在自己的命名空间里,所以它的/var/run/docker.sock和宿主机的/var/run/docker.sock不是同一个文件。这才是 snap 版 docker 能和平共处的核心机制——它把 socket 文件也隔离开了。但 snap 的 controller 接口会通过docker命令重定向到宿主机的 socket?不展开细说,因为不同版本的 snap docker 行为不太一样。我要强调的是:在宿主机视角看,跑着两个 dockerd,但在默认 socket 上只有一个监听者。
最终这台机器的情况就是:
/usr/local/bin/docker:手动安装的客户端二进制,优先级最高,平时敲 docker 用的就是它。/usr/bin/dockerd:apt 安装的守护进程,监听着/var/run/docker.sock,是实际对外服务的。/snap/bin/docker和 snap dockerd:snap 安装的完整套件,由于命名空间隔离,自成一派,和 apt 版互不干扰。但两个都存在,资源浪费,且后续升级混乱。
处理方式:卸载 snap 版,保留 apt 版,清理手动安装的/usr/local/bin/docker,让系统只保留一套标准路径的 docker。
7. 写一行脚本自动汇总:Ubuntu 系统里有几套 Docker
鉴于手动排查步骤多,我写了个简单的 Bash 脚本,可以一次性输出关键信息。你直接复制到服务器上执行即可,不依赖额外工具。
#!/bin/bash echo "===== 可执行文件路径 =====" type -a docker 2>/dev/null || which docker echo -e "\n===== 当前实际调用的 Docker =====" which docker echo -e "\n===== Docker 客户端与服务端版本 =====" docker version 2>&1 | sed -n '1,20p' echo -e "\n===== systemd 服务状态 =====" systemctl list-unit-files 2>/dev/null | grep -i docker systemctl status docker --no-pager 2>&1 | head -n 15 echo -e "\n===== snap 服务状态 =====" snap list 2>/dev/null | grep -i docker snap services docker 2>/dev/null echo -e "\n===== 监听中的 docker socket =====" sudo ss -xlp 2>/dev/null | grep -i docker || echo "未检测到 docker socket" echo -e "\n===== dockerd 进程 =====" ps aux | grep -E "[d]ockerd" || echo "无 dockerd 进程" echo -e "\n===== 常见数据目录 =====" sudo ls -ld /var/lib/docker /var/snap/docker/common/var-lib-docker 2>/dev/null执行后你会得到一份汇总。我建议重点关注三块:
type -a docker列出了几个路径,决定 shell 会调用哪一个。systemd和snap服务是否同时 active。ss -xlp里看到底是谁在监听默认 socket。
如果输出显示有多个可执行文件路径,或者 systemd 和 snap 服务同时存在,那就说明系统里不止一套 docker。定位到之后,你再按需清理保留目标版本。
8. 发现多套 Docker 之后怎么收敛成“只有一套”
8.1 先确定你想保留哪一套
大多数生产环境推荐保留 apt 官方源安装的 docker-ce,理由很直接:
- systemd 管理,开机自启,日志集成方便。
- 数据目录明确在
/var/lib/docker。 - 升级路径清晰,和 Ubuntu 原生的安全更新机制对齐。
- 不需要 snap 自带的那一套隔离层,排查网络问题时少一个变量。
snap 版 docker 本身也不是不能用,但它在磁盘占用、网络隔离、日志查看上给我的体验都不如 systemd 版本直观,而且不少第三方监控脚本默认读/var/lib/docker数据,snap 版还要额外适配路径。
8.2 卸载多余版本的正确姿势
卸载 snap 版:
sudo snap stop docker sudo snap remove docker注意保留数据目录的话,先手动备份/var/snap/docker/common/var-lib-docker。
卸载 apt 版:
sudo systemctl stop docker docker.socket sudo apt purge docker.io docker-ce docker-ce-cli containerd.io手动二进制安装的清理:
sudo rm -f /usr/local/bin/docker /usr/local/bin/dockerd /usr/local/bin/docker-compose sudo rm -rf /usr/local/lib/docker清理之后重新检查一次:
type -a docker which docker docker version systemctl status docker如果一切正常,你应该看到可执行文件只有一个路径,systemd 服务是唯一的管理者,docker version的 Client 和 Server 版本一致或能对应到同一套安装。
提示:执行卸载前,建议用
docker ps -a先看看现有容器是否保留。purge卸载 docker 不会自动删除/var/lib/docker里的容器数据,但如果你执行了apt autoremove或者手动清了目录,数据就没了。这种操作比卸载本身更危险。我的习惯是:把/var/lib/docker整个 tar 打包一份扔到备份盘再动包管理工具。
8.3 最后验证:从一个“干净状态”确认只剩一套
收敛完成后,我一般会做 10 秒验证:
docker version docker run --rm hello-world systemctl is-active docker如果 hello-world 能正常跑起来,systemd 又是 active,这台机器就回归到标准状态了。之后你再排查任何 Docker 相关的问题,思路都不用绕弯。
9. 查“几个 docker”这个动作本身的价值
很多人觉得“系统里有几个 docker”是一个伪命题——不就是一条命令的事吗?实际经历过后你会发现,这背后的信息量非常大:它告诉你这台机器的 Docker 是怎么来的、由谁在管、数据存在哪、升级走什么通道、出了问题日志去哪看。这些信息平时用不到,一旦涉及排障、迁移、审计,就全是关键线索。
我自己现在拿到任何一台 Ubuntu 机器的第一件事就是跑一遍第 7 节那个脚本,花两分钟把 Docker 的“家庭背景”摸清楚。别等出了问题再查,那时候你已经站在别人踩过的坑里了。