Docker 基础体系 · 第 64/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker Engine 升级:兼容、存储驱动、API、灰度、回滚和验证
Docker Engine 升级不是简单地替换一个二进制文件。一个正在运行的 Linux Docker 主机至少包含以下状态:
docker CLI / Compose / Buildx
│
│ Unix socket 或 TCP/TLS
▼
Docker daemon
│
├── containerd / runc
├── 镜像与容器元数据
├── 存储驱动与 data-root
├── 网络、卷、插件
└── 日志、事件、运行中容器
因此升级同时影响四类兼容性:
- 客户端与 Daemon 的 API 兼容性;
- Daemon 与 containerd、runc、内核及文件系统的兼容性;
- 镜像、容器、卷和网络等持久化状态的兼容性;
- 业务负载与升级期间故障、重启、回滚之间的兼容性。
本文只讨论 Linux 容器边界。Windows 容器、Docker Desktop 内部虚拟机、第三方发行版打包方式可能具有不同的升级和存储约束。
一、先区分升级对象:CLI、Daemon、运行时和业务镜像
Docker 常被当作一个软件包理解,但实际至少有四个独立的版本边界:
| 组件 | 作用 | 升级影响 |
|---|---|---|
| Docker CLI | 解析 docker 命令并调用 API |
影响命令语义与 API 请求 |
Docker Engine / dockerd |
管理容器、镜像、网络、卷和 API | 影响主机上的容器生命周期 |
| containerd、runc | 管理容器运行时和进程 | 影响容器创建、启动、停止 |
| 镜像构建与编排工具 | BuildKit、Buildx、Compose | 影响构建结果、API 调用和部署模型 |
安装包名称也不一定相同。官方 Docker 软件源通常包含 docker-ce、docker-ce-cli、containerd.io 等包,但不同 Linux 发行版可能使用发行版自己的 docker.io、独立的 containerd 包或不同的 systemd 单元。升级前必须先确认实际安装来源,而不是只根据命令名判断。
docker version
docker info
command -v dockerd
systemctl cat docker
dpkg -l | grep -E 'docker|containerd|runc' # Debian/Ubuntu
rpm -qa | grep -E 'docker|containerd|runc' # RPM 系发行版
典型的 docker version 输出包含:
Client:
Version: ...
API version: ...
Buildx version: ...
Server:
Engine:
Version: ...
API version: ...
Minimum API version: ...
containerd version: ...
runc version: ...
这里的 Client 和 Server 版本可以不同。升级 Docker CLI 并不等于升级 Daemon,升级 Daemon 也不一定自动升级 Compose 或 Buildx。
业务镜像则是另一条版本线。升级 Engine 通常不应被理解为“重新构建所有镜像”。镜像由内容寻址的层组成,容器由镜像层、可写层、配置和运行时状态共同组成。只要存储格式和运行时接口仍然兼容,已有镜像通常可以继续使用;但这不是对所有未来版本和所有插件的绝对保证。
二、Docker API 兼容:版本协商决定请求能否成立
2.1 API 版本的三个值
Daemon 的 /version 接口通常会返回:
ApiVersion:Daemon 支持的最高 API 版本;MinAPIVersion:Daemon 支持的最低 API 版本;Version:Engine 版本。
可以直接通过 Unix socket 查询:
sudo curl --unix-socket /var/run/docker.sock \
http://localhost/version | jq
输出结构类似:
{
"Version": "27.x",
"ApiVersion": "1.xx",
"MinAPIVersion": "1.yy",
"Os": "linux",
"Arch": "amd64"
}
Docker 客户端发起请求时,不是任意版本都能使用。设:
- :客户端希望使用的 API 版本;
- :Daemon 支持的最高 API 版本;
- :Daemon 支持的最低 API 版本。
请求至少需要满足:
如果客户端启用了 API 协商,客户端会尝试选择一个双方都支持的版本。理想情况下:
并且还要满足:
例如:
客户端支持:1.41
Daemon 支持:1.24 ~ 1.43
协商结果:1.41
如果:
客户端支持:1.20
Daemon 支持:1.24 ~ 1.43
则双方没有交集,客户端无法正常调用该 Daemon。
2.2 API 兼容不等于功能兼容
即使 API 版本落在可接受范围内,也不代表所有功能都存在。某些字段、参数和行为可能只在较新的 API 中提供。例如,CLI 能否使用某个选项,取决于:
- CLI 是否认识该选项;
- 协商后的 API 是否支持对应字段;
- Daemon、containerd 和运行时是否实现该操作;
- 当前对象状态是否允许执行该操作。
因此,下面两种错误含义不同:
client is newer than server
通常表示 API 版本或功能边界不匹配。
unknown flag: ...
可能是 CLI 本身版本过旧,也可能是命令属于 Buildx、Compose 等独立组件而不是旧版 Docker CLI。
检查实际请求版本:
docker version
docker info
env | grep '^DOCKER_'
DOCKER_HOST 会改变请求目标,例如从本地 Unix socket 切换到远端 TCP;DOCKER_API_VERSION 会强制使用指定 API 版本。强制设置后,客户端可能不再正常协商,因此它适合诊断或兼容旧环境,不适合无理由长期固定。
例如:
DOCKER_API_VERSION=1.41 docker version
如果该命令失败,而普通的 docker version 成功,说明环境变量或固定 API 版本正在限制客户端,而不是 Daemon 本身不可用。
2.3 TLS 远程 API 还增加了一层连接兼容性
远程 Daemon 的兼容性不只有 API 版本,还包括:
DOCKER_HOST指向的地址;- TLS 是否启用;
- CA、客户端证书和私钥是否匹配;
- 服务端证书的主机名是否包含目标地址;
- 防火墙和 systemd socket 是否允许连接。
例如:
export DOCKER_HOST=tcp://docker.example.internal:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH="$HOME/.docker/docker-example"
docker version
如果没有 TLS,直接将 Daemon 监听在可访问网络上的 2375 端口,会把 Docker 控制权暴露给能够连接该端口的主体。Docker API 权限基本等价于主机上的高权限控制权,因为 API 可以创建挂载宿主机路径的特权容器。升级时修改监听地址或 TLS 配置,必须作为独立的连接验证步骤处理。
三、升级前必须识别的持久化状态
docker ps 只显示容器,不等于显示主机上所有需要保护的数据。至少要盘点:
docker ps -a
docker images --digests
docker volume ls
docker network ls
docker plugin ls
docker system df -v
需要分别记录:
- 正在运行和已停止的容器;
- 镜像名称与 digest;
- 容器的挂载、环境变量、重启策略和网络;
- 命名卷及其实际使用者;
- 自定义网络的子网和连接关系;
- 日志驱动及日志路径;
- registry、代理、TLS、镜像源配置;
- Swarm、插件、GPU 运行时等额外能力;
- Compose 文件和
.env文件; - systemd drop-in 与
/etc/docker/daemon.json。
容器的可写层不是可靠的业务数据层。升级前即使不迁移 data-root,也应该将数据库数据放在卷或宿主机挂载路径中,并使用数据库自身的备份机制。卷数据也不能简单等同于一致性备份:正在写入的数据库卷被直接复制,可能得到逻辑上不一致的文件集合。
一个基本的卷文件备份示例:
docker run --rm \
-v app_data:/source:ro \
-v "$PWD/backup":/backup \
alpine \
tar czf /backup/app_data.tgz -C /source .
这个命令只保证“容器内看到的文件被读取并打包”,不保证数据库事务一致性。对于 PostgreSQL、MySQL 等数据库,应该先执行数据库原生备份,或在应用停止、冻结写入后再复制卷。
四、data-root、存储驱动和文件系统不是同一个概念
4.1 data-root 保存的是 Docker 的状态
Docker 默认将大量状态放在 /var/lib/docker。可以在 daemon.json 中修改:
{
"data-root": "/srv/docker"
}
data-root 是 Docker 状态目录的位置,不是存储驱动名称。修改它意味着 Docker 将从另一个目录读取:
- 镜像层;
- 容器元数据;
- 容器可写层;
- 网络和卷相关状态;
- 构建缓存等部分状态。
如果只把 data-root 改到空目录,Docker 看起来会像一台新主机:镜像、容器和卷都不会自动出现。正确迁移需要先停止 Daemon,完整复制或恢复整个数据目录,并保持权限、符号链接、扩展属性和底层文件系统语义。
sudo systemctl stop docker
sudo rsync -aHAX --numeric-ids \
/var/lib/docker/ /srv/docker/
sudo install -d -m 0755 /etc/docker
sudoedit /etc/docker/daemon.json
sudo systemctl start docker
docker info --format 'Root={{.DockerRootDir}}'
rsync 的 -A 和 -X 用于保留 ACL 与扩展属性,--numeric-ids 避免迁移时按名称重新映射 UID/GID。执行复制时不能让旧 Daemon 继续写入源目录,否则复制结果可能缺少刚写入的元数据,形成不一致状态。
更安全的做法是:
- 先做一次停止前的预复制;
- 停止 Docker;
- 再做一次最终增量同步;
- 修改配置;
- 启动并验证;
- 保留旧目录,直到恢复演练完成。
4.2 存储驱动决定层如何落盘
存储驱动负责把镜像只读层、容器可写层和层之间的联合视图映射到宿主机文件系统。检查当前驱动:
docker info --format \
'Driver={{.Driver}}
BackingFilesystem={{.BackingFilesystem}}
DockerRootDir={{.DockerRootDir}}'
现代 Linux 安装通常优先使用 overlay2,但“支持 overlay2”并不只取决于 Docker 版本,还取决于:
- 内核是否支持所需 OverlayFS 能力;
- 底层文件系统是否满足要求;
- XFS 是否启用了正确的
ftype; - rootful 或 rootless 模式的限制;
- 是否存在网络文件系统等不适合作为容器层存储的后端。
XFS 的检查示例:
findmnt -no FSTYPE,OPTIONS /var/lib/docker
xfs_info "$(findmnt -no TARGET /var/lib/docker)" | grep ftype
对于 overlay2,XFS 通常需要 ftype=1,也就是目录项类型信息可用。若底层条件不满足,可能出现 Daemon 启动失败、镜像拉取失败、容器启动失败,或者更隐蔽的权限和文件行为异常。不能只看 docker info 中显示了 overlay2 就认为所有功能都已经验证。
4.3 不要在原地切换存储驱动
存储驱动不是一个可以随意修改的运行参数。假设旧目录按驱动 A 组织,启动新 Daemon 时改为驱动 B:
{
"storage-driver": "overlay2"
}
新驱动通常无法直接解释旧驱动的层目录。即使 Docker 成功启动,也可能只看到空的镜像和容器状态,或者产生不可预期的混合数据。
正确边界是:
- 只迁移
data-root:保持存储驱动不变,复制整个 Docker 数据目录; - 更换存储驱动:新建目标 Docker 状态目录,使用镜像重新拉取或通过镜像归档迁移,使用卷和数据库备份恢复业务数据;
- 不要把旧驱动目录逐层复制到新驱动目录。
镜像迁移可以使用:
docker image save myapp@sha256:... -o myapp.tar
docker image load -i myapp.tar
但 docker image save 只处理镜像,不处理容器的可写层、卷、网络和 Daemon 配置。它不是整机 Docker 备份。
4.4 overlay2 的真实边界
联合文件系统会让容器看到“多个只读层加一个可写层”的合并视图。容器首次修改只读层中的文件时,可能发生 copy-up:文件被复制到可写层后再修改。因此:
- 大文件的首次修改可能产生较大 I/O;
- 容器可写层不适合承载数据库主数据;
docker system df显示的镜像层和容器可写层应分别观察;- 直接修改 Docker 数据目录可能破坏层引用和元数据。
这也是“升级前清理空间”的风险来源。docker system prune 可能删除未使用的镜像、网络、停止容器,带有 --volumes 时还可能删除未使用卷。它不是升级的必要步骤,也不能代替备份。
五、配置升级:合并而不是覆盖
Docker Daemon 常见配置来源包括:
/etc/docker/daemon.json;- systemd 的
ExecStart; /etc/systemd/system/docker.service.d/*.conf;- 环境文件;
- 发行版默认 unit;
- 远程访问和 TLS 证书配置。
查看最终的 systemd 配置:
systemctl cat docker
systemctl show docker -p ExecStart -p Environment
检查 JSON 语法:
jq empty /etc/docker/daemon.json
但 JSON 合法不代表 Docker 配置有效。启动后仍要检查:
sudo systemctl restart docker
sudo systemctl --no-pager --full status docker
sudo journalctl -u docker -b --no-pager -n 200
docker info
一个常见错误是同一配置项同时出现在 daemon.json 和 systemd 启动参数中。例如,hosts、日志驱动等配置存在重复定义时,Daemon 可能拒绝启动或以难以预期的来源为准。升级前应记录旧配置,逐项合并新配置,而不是用供应商示例覆盖整个文件。
代理、镜像源、日志驱动、TLS 和 data-root 的修改也应分开验证。把多个高风险改动和 Engine 版本升级放在同一个变更中,会使失败后的归因和回滚都变得困难。
六、BuildKit、Buildx 和 Compose 的兼容边界
现代 Docker 构建通常使用 BuildKit。检查构建工具:
docker buildx version
docker buildx ls
docker compose version
BuildKit 负责构建图的求解、缓存、并行执行和输出;Buildx 是常用的 CLI 前端。构建失败不一定是 Engine API 失败,也可能来自:
- Dockerfile 语义变化;
- BuildKit 前端镜像或语法版本;
- registry 认证;
- 代理和 DNS;
--platform对应的模拟器或原生构建节点;- 缓存导入导出配置。
升级后至少编译一次代表性镜像:
docker buildx build \
--progress=plain \
--load \
-t upgrade-smoke:test \
.
--load 将单平台构建结果加载到本地 Docker 镜像存储,便于随后用 docker run 验证;多平台构建通常需要 --push 或其他输出方式,不能假设结果自动出现在本地镜像列表中。
Compose 现代规范中,顶层 version 字段已经不再用于选择 Compose 实现版本,某些版本会提示它已过时。验证 Compose 文件应使用:
docker compose config -q
docker compose config
config -q 主要验证解析和合并结果,不会证明:
- 镜像一定存在;
- 网络端口没有冲突;
- 卷权限正确;
- 应用能够建立数据库连接;
- 健康检查能够通过。
因此 Compose 验证必须继续到容器启动和业务探针:
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100
升级前应固定镜像 digest,而不是只记录可变的 latest 标签:
docker image inspect myapp:prod \
--format '{{index .RepoDigests 0}}'
这样可以区分“Engine 升级导致的行为变化”和“实际上拉到了另一份镜像”。
七、为什么生产上应优先灰度,而不是原地升级
灰度升级是先让少量、可隔离的主机或工作负载使用新 Engine,再逐步扩大范围。它与“在一台主机上先试一次”不同:真正的灰度需要能够控制流量、保留旧版本容量,并且有明确的比较指标。
推荐的主机状态转换如下:
stateDiagram-v2
[*] --> Inventory
Inventory --> BackupVerified
BackupVerified --> Drained
Drained --> Upgraded
Upgraded --> SmokeTested
SmokeTested --> CanaryServing
CanaryServing --> Expanded
CanaryServing --> RolledBack
Expanded --> [*]
RolledBack --> Restored
Restored --> [*]
关键路径是:
- Inventory:记录版本、配置、存储驱动、挂载、镜像 digest 和业务关系;
- BackupVerified:备份不仅存在,还能读取或恢复到测试环境;
- Drained:从负载均衡器、调度器或集群中摘除主机;
- Upgraded:停止服务、更新软件包或 Daemon、启动服务;
- SmokeTested:验证 API、镜像、网络、卷和容器生命周期;
- CanaryServing:只承载有限流量或少量副本;
- Expanded:指标正常后逐批扩大;
- RolledBack:触发回滚条件,恢复旧 Engine 或旧主机;
- Restored:验证业务和数据恢复,而不是只验证进程存在。
如果只有单台主机且没有备用节点,严格意义上无法实现无损灰度,只能做维护窗口内的原地升级。此时更重要的是缩短停机路径、先完成恢复演练,并确认业务能够从备份重建。
灰度需要保留旧版本容量。若集群总容量为 ,正在升级的主机需要消耗 ,业务高峰需要的安全容量为 ,则至少应满足:
这不是 Docker 的规范,而是容量约束。若摘除一台主机后剩余容量不足,升级动作本身就会制造调度失败或业务过载。
八、可执行的单机升级流程
下面给出适用于 systemd 管理、官方软件源安装的 Linux 主机流程。具体包名和版本格式随发行版变化,不能直接把示例版本复制到所有环境。
8.1 记录升级前基线
mkdir -p /var/tmp/docker-upgrade-before
docker version \
> /var/tmp/docker-upgrade-before/docker-version.txt
docker info \
> /var/tmp/docker-upgrade-before/docker-info.txt
docker ps -a --no-trunc \
> /var/tmp/docker-upgrade-before/containers.txt
docker images --digests \
> /var/tmp/docker-upgrade-before/images.txt
docker volume ls \
> /var/tmp/docker-upgrade-before/volumes.txt
docker network ls \
> /var/tmp/docker-upgrade-before/networks.txt
sudo cp -a /etc/docker \
/var/tmp/docker-upgrade-before/etc-docker
sudo systemctl cat docker \
> /var/tmp/docker-upgrade-before/docker-unit.txt
再检查正在运行的容器是否有重启策略:
docker ps -q | xargs -r docker inspect \
--format '{{.Name}} restart={{.HostConfig.RestartPolicy.Name}}'
重启策略只能帮助容器重新启动,不能证明应用已恢复。应用仍可能因为数据库未就绪、配置错误或卷权限变化而处于不可用状态。
8.2 先验证备份和恢复路径
对于数据库执行原生备份;对于卷可以在测试主机恢复:
docker volume create restore_test
docker run --rm \
-v restore_test:/target \
-v "$PWD/backup":/backup \
alpine \
sh -c 'tar xzf /backup/app_data.tgz -C /target && find /target -maxdepth 2 -type f | head'
这一步验证的是归档可读、卷可写和基本文件可见性。它仍不替代数据库启动后的完整一致性检查。
8.3 选择目标版本并检查可用包
以 Debian/Ubuntu 为例:
apt-cache madison docker-ce
apt-cache madison docker-ce-cli
apt-cache madison containerd.io
应该选择同一软件源中相互匹配的版本组合,并阅读该版本对应的发行说明。不要只降级 docker-ce 而留下不匹配的 CLI、containerd 或运行时包。
8.4 摘流、停止和升级
若主机承载业务,先从负载均衡器或调度器摘除。然后:
sudo systemctl stop docker
sudo systemctl is-active docker || true
确认没有计划任务或自动化系统在停止期间继续执行 docker 命令。需要复制 data-root 时,在此阶段进行最终同步。
升级包的命令取决于发行版和仓库。例如使用 APT 时,实际版本号应来自 apt-cache madison:
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
如果需要固定到指定版本,应使用仓库中真实存在且相互匹配的完整版本字符串,而不是手写一个看似合理的版本号。
启动并检查:
sudo systemctl daemon-reload
sudo systemctl start docker
sudo systemctl --no-pager --full status docker
sudo journalctl -u docker -b --no-pager -n 200
daemon-reload 只在 systemd unit 或 drop-in 发生变化时有意义;它不会刷新 daemon.json 的语义校验,真正的配置检查仍发生在 Daemon 启动时。
九、验证必须覆盖控制面、运行时、数据面和业务面
“systemctl status docker 显示 active”只证明 Daemon 进程启动了。完整验证至少分四层。
9.1 控制面:API 和配置
docker version
docker info
docker events --since 1m --until 2m 2>/dev/null || true
核对:
- Server 版本是否为目标版本;
- API 版本和最小 API 版本是否符合客户端;
DockerRootDir是否正确;Storage Driver是否仍为预期值;- 日志驱动、代理、镜像源和 TLS 是否生效;
- containerd、runc 是否为预期版本。
9.2 运行时:容器生命周期
使用一个已存在且无破坏性的测试镜像,或准备专用 smoke-test 镜像:
docker run --rm \
--name engine-upgrade-smoke \
alpine:3.20 \
sh -c 'uname -a; id; test -f /etc/os-release'
这个测试验证镜像解包、容器创建、runc 启动、进程退出和清理路径。它不能验证业务镜像的动态链接库、启动脚本和权限,因此还应启动一份代表性业务容器。
检查停止、重启和日志:
docker run -d --name restart-smoke alpine:3.20 \
sh -c 'while true; do echo alive; sleep 5; done'
docker restart restart-smoke
docker logs --tail=20 restart-smoke
docker rm -f restart-smoke
如果生产配置使用特定日志驱动,应该检查实际日志是否仍能被采集,而不是只看 docker logs。
9.3 数据面:卷、网络和镜像
卷验证:
docker volume create upgrade-smoke-volume
docker run --rm \
-v upgrade-smoke-volume:/data \
alpine:3.20 \
sh -c 'echo verified > /data/check.txt'
docker run --rm \
-v upgrade-smoke-volume:/data:ro \
alpine:3.20 \
cat /data/check.txt
docker volume rm upgrade-smoke-volume
网络验证:
docker network create upgrade-smoke-net
docker run -d --name smoke-server \
--network upgrade-smoke-net \
alpine:3.20 \
sh -c 'while true; do printf "ok\n"; sleep 1; done'
docker inspect smoke-server \
--format '{{json .NetworkSettings.Networks}}' | jq
docker rm -f smoke-server
docker network rm upgrade-smoke-net
更有价值的网络测试是使用与生产相同的端口、DNS、代理和自定义网络条件,验证服务之间的真实连接,而不是只验证容器能启动。
9.4 业务面:健康检查和关键事务
业务验证应至少包括:
- HTTP 或 gRPC 健康检查;
- 读取一条关键配置;
- 访问数据库;
- 写入并读取一条测试数据,或执行只读的业务探针;
- 检查错误率、延迟、重启次数和日志采集;
- 检查监控、告警和审计事件是否仍然到达。
健康检查通过不一定代表业务可用。例如应用进程能返回 200,但异步消费者已经停止,或数据库连接池全部失败。灰度期间应比较新旧主机的相同指标,而不是只观察新主机是否“没有明显报错”。
十、回滚:回滚软件包不等于回滚数据
10.1 回滚有三个不同层次
- 流量回滚:把请求切回旧主机或旧副本;
- 工作负载回滚:恢复旧镜像 digest、旧 Compose 配置或旧部署版本;
- Engine 回滚:把 Daemon 和其依赖包恢复到旧版本。
优先级通常是先做流量回滚,再判断是否需要 Engine 回滚。因为 Engine 升级失败时,旧主机仍然是最小风险的业务恢复路径。
10.2 什么时候包降级不够
如果新 Engine 修改或升级了 Docker 数据目录中的元数据格式,旧 Engine 不一定能读取新状态。此时直接执行:
sudo apt-get install docker-ce=<old-version>
sudo systemctl restart docker
可能导致启动失败、对象缺失或更严重的状态不一致。具体可回退性取决于实际版本、发行版打包、containerd、存储驱动和数据目录状态,不能假设所有版本都支持任意方向降级。
可靠的 Engine 回滚需要预先保存:
- 旧版 Engine、CLI、containerd、runc 包;
/etc/docker和 systemd drop-in;- 旧的 Docker 数据目录快照;
- 卷和数据库的一致性备份;
- 镜像 digest 与 Compose 配置。
理想回滚路径是:
停止新 Daemon
│
├── 若旧数据目录仍兼容:使用旧包启动并验证
│
└── 若数据目录可能已变更:
恢复升级前的 data-root 快照
恢复配置和软件包
启动旧 Daemon
恢复或校验业务数据
如果只有“升级前复制的部分文件”,而没有完整的 data-root 和业务数据备份,就不能把它称为可验证的回滚方案。
10.3 数据库迁移会阻断应用回滚
Engine 回滚和数据库 schema 回滚是两个独立问题。例如新版本应用执行了不可逆的数据库迁移,即使 Engine 成功降级,旧应用也可能无法读取新 schema。因此灰度发布还必须考虑:
- 应用是否向后兼容数据库;
- 数据迁移是否可逆;
- 新旧版本是否会同时访问同一卷;
- 回滚时是否需要恢复数据库快照;
- 是否需要采用 expand/contract 迁移策略。
Docker 只负责容器和卷的生命周期,不会自动为数据库提供事务级回滚。
十一、常见失败表现与诊断路径
Daemon 启动失败
先看 systemd 和本次启动日志:
systemctl status docker
journalctl -u docker -b --no-pager
dockerd --validate 2>&1 || true
具体验证命令是否可用取决于 Engine 版本;即使命令不存在,也不能说明配置有效。最可靠的证据仍是 Daemon 日志中的解析错误、重复配置、权限错误或存储驱动错误。
常见原因包括:
daemon.jsonJSON 语法错误;- 配置项在 JSON 和 systemd 参数中重复;
data-root不存在或权限错误;- 底层文件系统不满足存储驱动条件;
- 旧插件或运行时依赖不兼容;
- 监听地址或 TLS 证书配置错误。
容器能创建但不能启动
查看容器状态和事件:
docker ps -a
docker inspect <container>
docker logs <container>
docker events --since 10m
如果日志为空,可能是进程在日志初始化前退出、日志驱动不可用、权限错误,或错误实际出现在 Daemon 日志中。应同时查看:
journalctl -u docker -b --no-pager
API 错误但 Daemon 正常
docker version
env | grep '^DOCKER_'
docker context ls
docker context inspect "$(docker context show)"
重点检查当前 CLI 到底连接了哪个 Daemon。很多“升级无效”的问题其实是 CLI 仍连接旧的远程 context,或者 DOCKER_HOST 覆盖了预期目标。
镜像拉取失败
升级后 registry 失败不一定是 Engine API 问题,可能是:
- 认证配置没有迁移;
- 代理配置只写在 shell 中,Daemon 进程没有继承;
- CA 证书路径改变;
- DNS 或防火墙改变;
- 镜像源配置格式不再接受;
- 架构或平台选择发生变化。
应分别测试 Daemon 能否访问 registry、当前用户的凭据是否存在,以及目标镜像是否包含所需平台。
卷存在但应用看不到数据
先确认容器实际挂载:
docker inspect <container> \
--format '{{json .Mounts}}' | jq
然后检查:
- Compose 项目是否创建了另一个同名但不同作用域的卷;
data-root是否指向正确目录;- 容器内 UID/GID 是否能访问文件;
- SELinux 标签或其他安全策略是否阻止访问;
- 应用是否使用了错误的挂载目标。
“docker volume ls 中能看到卷”只证明 Daemon 的元数据中存在该卷,不证明该卷已挂载到目标容器,也不证明应用用户有权限读取。
十二、live-restore 与重启策略不能代替灰度
Docker 的 live-restore 可以在 Daemon 短暂不可用时尽量保持部分运行中容器继续运行,但它不是任意 Engine 升级的无中断保证。升级可能涉及 containerd、runc、存储驱动、网络初始化或配置重载;这些路径并不都能由 live-restore 覆盖。
同样,restart: unless-stopped 或 Docker 的重启策略只能解决“容器进程退出后是否重新拉起”,不能解决:
- 镜像拉取失败;
- 卷挂载错误;
- 数据库尚未就绪;
- 应用自身健康检查失败;
- 网络和证书配置错误;
- 数据已经损坏或 schema 不兼容。
因此,重启策略是故障恢复机制的一部分,不是升级验证和回滚方案。
十三、一份可落地的升级验收标准
升级完成不能只写“服务已恢复”。应保存可比较的证据:
[ ] Server Engine 版本符合目标版本
[ ] Client 与 Server API 存在有效交集
[ ] DockerRootDir 未意外改变
[ ] Storage Driver 和底层文件系统符合预期
[ ] containerd、runc 版本和运行时调用正常
[ ] daemon.json、systemd、代理、TLS、镜像源配置已验证
[ ] 代表性镜像可以拉取和启动
[ ] BuildKit/Buildx 可以完成代表性构建
[ ] Compose 配置可以解析并启动
[ ] 关键卷挂载正确,数据抽样可读写
[ ] 自定义网络、DNS、端口和服务发现正常
[ ] 日志和监控链路正常
[ ] 健康检查和关键业务探针通过
[ ] 新主机已承载灰度流量并达到观察窗口
[ ] 回滚包、旧配置、data-root 快照和业务备份仍可使用
其中“观察窗口”应覆盖业务真实的周期性行为,例如定时任务、消息消费、日志轮转、备份任务和自动扩缩容。刚启动几分钟没有错误,不能证明这些异步路径正常。
结语:把 Engine 升级看成状态迁移
Docker Engine 升级的核心不是安装新版本,而是把以下状态从旧实现安全地迁移到新实现:
升级成功需要同时满足:
回滚成功则还需要:
所以,最稳妥的升级方案通常是:先盘点兼容边界,保持存储驱动不变,验证 data-root 和备份,隔离并灰度少量主机,按控制面、运行时、数据面和业务面逐层验收,并把流量回滚、应用回滚和 Engine 回滚分别设计。这样即使新版本出现问题,也能明确知道该退回哪一层,而不是盲目地重新安装一个旧软件包。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker Live Restore 与重启恢复:Daemon 故障、容器存活和限制
- 下一篇:Docker 离线与受限网络:镜像同步、依赖缓存、签名和补丁
- 延伸:Docker Daemon 配置:data-root、日志、代理、镜像源、TLS 和验证
- 延伸:Docker 数据备份与主机迁移:卷、数据库、一致性和恢复演练
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论