
Docker 的版本迭代速度很快,每几个月就会有一个新的稳定版发布,其中可能包含安全补丁、存储驱动改进或者网络插件的新特性。但在生产环境里,直接执行 apt-get upgrade docker-ce 或者 yum update docker-ce 并不总是安全的。最常见的事故包括:新版本 Docker 守护进程与旧版客户端 API 不兼容、容器存储驱动从 overlay 迁移到 overlay2 失败导致所有容器无法启动、以及升级过程中 /var/lib/docker 目录被部分改写造成镜像层损坏。因此,在动手升级之前,就要明确回滚路径,不能等到服务挂了才想办法。
回滚方案的核心不是“把二进制换回旧版本”这么简单。Docker 的数据目录、配置文件、系统服务单元以及网络命名空间都是相互关联的。如果只替换二进制而不同步恢复 /var/lib/docker 中的元数据,旧版守护进程可能无法识别新版写入的镜像引用,甚至会报出 “incompatible daemon version” 之类的错误。所以,一个靠谱的回滚方案必须涵盖升级前的快照、失败时的恢复步骤、以及恢复后的验证手段。
升级前必须做的三件事
第一件事是备份 Docker 数据目录。Docker 默认把所有镜像、容器、卷和网络配置存放在 /var/lib/docker 下。虽然这个目录可能很大,但对于回滚来说它是最关键的状态文件。最简单的方式是使用 rsync -aAX /var/lib/docker /var/lib/docker.bak.$(date +%Y%m%d) 复制一份。如果磁盘空间紧张,至少也要把 /var/lib/docker/image 和 /var/lib/docker/containers 这两个子目录备份下来,它们分别存放镜像层元数据和运行中容器的配置。注意备份时最好先暂停 Docker 服务,否则正在写入的数据会导致备份不一致。
第二件事是记录当前版本和关键配置。执行 docker version 和 docker info,把输出保存成文本文件。特别留意 Storage Driver 字段,比如 overlay2 或 devicemapper。同时把 /etc/docker/daemon.json 的内容复制到备份目录。如果你的 Docker 是通过 systemd 管理的,还要备份 /etc/systemd/system/docker.service.d/ 下的覆盖配置,以及 /lib/systemd/system/docker.service 文件本身。这些文件里可能包含了自定义的 --data-root 参数、镜像加速器地址或者日志驱动选项。
第三件事是导出正在运行的容器。虽然升级 Docker 通常不需要重建容器,但一旦回滚后出现容器无法恢复的情况,手头有一个镜像导出包就能快速重建关键服务。对每一个重要的容器执行 docker export <容器名> -o <容器名>.tar,或者使用 docker save 导出镜像。注意 docker export 只导出容器文件系统,不包含卷数据;对于有持久化需求的容器,还要单独备份挂载卷。
三种回滚路径的实操对比
路径一:使用包管理器的降级安装。如果你当初是用 apt 或 yum 安装的 Docker,理论上可以用 apt-get install docker-ce=<旧版本号> 来降级。例如在 Ubuntu 上先查看可用版本:apt-cache madison docker-ce,然后指定版本执行安装。这种方式的好处是自动处理依赖关系,但坏处是它可能不会恢复 /var/lib/docker 中已经被新版修改过的元数据。如果升级过程中守护进程已经启动过一次并执行了存储驱动的迁移,降级安装后旧版守护进程可能会因为无法读取新版写的格式而崩溃。
路径二:二进制替换加配置文件恢复。这种方案适合离线环境或者不想依赖包管理器的场景。先停止 Docker 服务,然后把备份的旧版二进制文件(比如 /usr/bin/docker、/usr/bin/dockerd、/usr/bin/containerd 等)直接复制回原位。同时把之前备份的 /var/lib/docker 整个目录覆盖回去。注意覆盖前必须先删除当前的 /var/lib/docker,不能做增量合并,否则新旧文件混在一起会导致各种诡异问题。覆盖完成后,把 daemon.json 和 systemd 单元文件也恢复,最后执行 systemctl daemon-reload 并启动 Docker。
# 停止 Docker 服务 sudo systemctl stop docker # 备份当前(有问题的)数据目录,保留现场 sudo mv /var/lib/docker /var/lib/docker.broken # 恢复升级前的数据目录 sudo mv /var/lib/docker.bak.20250601 /var/lib/docker # 恢复旧版二进制文件 sudo cp /backup/docker-20.10.24/docker /usr/bin/docker sudo cp /backup/docker-20.10.24/dockerd /usr/bin/dockerd sudo cp /backup/docker-20.10.24/containerd /usr/bin/containerd # 重新加载 systemd 配置并启动 sudo systemctl daemon-reload sudo systemctl start docker
路径三:使用 Docker 官方提供的静态二进制包重装。如果你升级时没有保存完整的旧版二进制文件,可以去 Docker 官方 GitHub 仓库的 release 页面下载对应版本号的 docker-<版本>.tgz 压缩包。解压后把里面的二进制文件覆盖到 /usr/bin 下。这种方式和路径二类似,唯一区别是二进制文件来源不同。但要注意,静态包里的 docker 客户端和 dockerd 守护进程必须匹配,不能混用不同版本。
回滚后不要立即重启所有容器。先执行 docker info 检查存储驱动是否正常,再运行 docker ps -a 看容器列表是否能正确读取。如果出现 “Error response from daemon: unknown storage driver” 之类的错误,说明数据目录恢复不完整,需要重新从备份恢复。另外,如果之前升级时执行过 docker system prune 或者删除了旧镜像,回滚后这些镜像不会自动回来,需要使用之前导出的镜像包重新 docker load。
回滚后的验证与预防措施
验证回滚是否成功,不能只看 Docker 服务有没有启动。首先要检查镜像层引用是否一致:执行 docker images -q | xargs docker inspect --format '{{.Id}}',确认每个镜像 ID 都能在 /var/lib/docker/image/overlay2/imagedb/content/sha256/ 目录下找到对应文件。然后启动一个测试容器(比如 docker run --rm hello-world),观察网络、DNS 和存储驱动是否正常工作。对于有自定义网络的容器,执行 docker network inspect bridge 检查网络配置是否完整。
如果回滚后仍然无法启动关键容器,可以利用之前导出的容器 tar 包恢复。先创建一个空容器,然后 docker import 导入文件系统,再通过 docker commit 生成新镜像。不过这种方式会丢失容器原本的启动命令和环境变量,所以更好的办法是在升级前就保存每个容器的 docker inspect 输出。把这些 JSON 文件备份下来,回滚失败时可以对比容器的配置差异,手动重建启动参数。
为了避免频繁升级带来的回滚压力,建议在生产环境采用“先测试、后生产”的节奏。先在独立的测试节点上执行升级,运行完整的冒烟测试(包括容器创建、删除、网络联通、卷挂载),确认无误后再对生产节点操作。同时在 CI/CD 流水线中加入 Docker 版本检查步骤,把当前版本号写入监控系统。如果升级后监控发现容器重启次数突然增加或者镜像拉取失败,就自动触发回滚脚本。脚本可以封装成 systemd 服务或者 Ansible playbook,把备份、停止服务、恢复数据、启动验证整个流程标准化。