前两天帮朋友处理一台跑了好几年项目的 Ubuntu 20.04 服务器,docker --version一看还是 19.03,机器上 MySQL、Redis、GitLab 一堆容器跑着,还有两个和 SLAM 仿真相关的环境容器,属于典型的“不敢动、又不得不动”。好在我把整体流程走了一遍,升级完容器一个没丢、数据卷完好、服务恢复得也很快,这才有底气把整个过程整理成这篇实战指南。
在 Ubuntu 20.04 下做 Docker 无缝升级,最怕的不是版本差异,而是升级前没想清楚“无缝”的含义、升级时踩中仓库源失效、daemon 配置不兼容、containerd 版本不匹配这些暗坑。这篇内容主要围绕三大块来写:升级前的准备和风险排查、升级中的命令与配置处理、升级后的完整验证方法,同时把我实际操作中遇到过的典型问题整理成一个速查表,方便你直接照着排查。如果你是第一次在生产环境升级 Docker,或者机器上已经积累了大量容器和数据,这篇文章值得通篇看完再动手。
1. 升级前先想清楚:为什么升、怎么才算“无缝”
1.1 升级 Docker 到底解决了什么问题
很多朋友会觉得 Docker 用得好好的,没必要频繁升级。这句话在个人开发机上有一定道理,但在 20.04 这种长期维护的系统上,长期不升级 Docker 的风险是实实在在的。
首先是安全问题。Docker 由 dockerd、containerd、runc 等多个组件组成,其中 runc 是底层容器运行时,一旦出现容器逃逸漏洞,攻击者可能直接从容器内部突破到宿主机。这类漏洞几乎都是通过升级 Docker 发行版来修复的,你如果一直锁在旧版本,相当于把这些已知漏洞长期暴露在外面。
其次是兼容性问题。现在的镜像构建工具、编排平台、监控采集组件都在往新版本推进,老版本 Docker 在 BuildKit、Compose V2、新的 API 接口上支持得不够好。比如新版 docker compose 插件需要 Docker Engine 20.10 以上的 API 版本,如果你还在 19.03 上,很多新特性用不了。再比如一些云平台、CI/CD 流水线默认调用的 API 字段在老版本里不存在,会导致自动化流程直接中断。
还有 Ubuntu 20.04 这个系统本身。它发布早,默认软件源里的 Docker 版本往往偏旧,很多教程让大家通过apt install docker.io装的其实是发行版自带的阉割版,和 Docker 官方源里的 docker-ce 不是一个版本体系。升级到官方维护的稳定版,修复 bug 的速度快很多,更新也跟得上节奏。
1.2 “无缝”指的是什么,很多人一开始就理解偏了
我在实际操作里发现,很多人听到“无缝升级”就以为整个升级过程中所有容器一秒都不停。这个理解太理想化了。Docker 升级本质上做的事情是:替换 dockerd、containerd、runc 这些二进制文件,然后重启 docker 守护进程。只要 daemon 重启,默认情况下容器就会跟着停掉再自动拉起,中间必然有一次中断。
要做到真正的无缝,关键在/etc/docker/daemon.json里有个配置叫live-restore。如果你在升级前已经把它设置为 true,那么即使 docker daemon 重启,容器进程本身不会被 kill,只是守护进程短暂跳一下,网络和存储层面也基本不受影响。这个机制我在生产环境里实际用过,简直是为升级场景量身定做的。
反过来说,如果你的 daemon.json 没开 live-restore,又要求业务完全无感,那就要提前评估能否接受一次秒级或分钟级的抖动。对数据库这类对连接中断敏感的容器,我一般建议不仅打开 live-restore,还要在非业务高峰窗口操作,并且提前做好数据库逻辑备份,双保险。
1.3 先做一次风险摸底,列好检查清单再动手
升级前我会先把可能翻车的点列一遍,确认每一项都心里有数。下面这张表在多次升级实践中帮我避了不少坑,分享出来给你参考:
| 风险点 | 可能造成的影响 | 应对方式 |
|---|---|---|
| /var/lib/docker 所在分区空间不足 | 升级后新镜像拉不下来、日志写满、容器启动异常 | 用 df -h 确认剩余空间,至少留出 20% 余量 |
| daemon.json 存在废弃配置 | dockerd 启动失败,服务直接不可用 | 升级前备份配置文件并逐项审查 |
| containerd.io 被 apt 锁定 | dockerd 连不上 containerd,启动报错 | 检查 apt-mark showhold,必要时解除锁定 |
| 自定义网桥和 iptables 规则冲突 | 端口映射失效、宿主机能访问但外部进不来 | 升级后逐项测试端口映射和容器互通 |
| registry-mirrors 地址失效 | 拉取新镜像超时失败 | 提前拉一个测试镜像验证地址可用性 |
这张表看着简单,但它背后对应的是升级失败最常见的原因。我见过太多人跑到一半卡住,最后发现都是这些基础项没检查。升级这件事,功夫全在动手之前。
2. 升级前准备:备份、体检、确认源
2.1 给容器、镜像、数据卷做一次“快照登记”
只要机器上还有一点跑着的业务,我都不建议直接执行升级命令。跳过的第一步,永远是把现状记录下来。
# 记录当前容器、镜像、数据卷信息 docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" > container_list_$(date +%F).txt docker image ls --format "table {{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}" > image_list_$(date +%F).txt docker volume ls > volume_list_$(date +%F).txt这三条命令生成的文本文件虽然不起眼,关键时刻能救命。比如升级后发现某个容器没有自动拉起,你能根据文件知道它原来叫什么名字、用的哪个镜像、端口映射规则是什么,几分钟就能手工重建,而不是对着空空的 docker ps 发呆。
数据卷备份这块,我的原则是:空间足够就全量打包,空间不够就至少做关键应用的逻辑备份。
sudo tar -czf docker_volumes_backup_$(date +%Y%m%d).tar.gz /var/lib/docker/volumes如果是数据库容器,我更推荐额外做一次逻辑备份。MySQL 用 mysqldump,Redis 用 BGSAVE 后拷贝持久化文件,这样即使数据卷在极端情况下损坏,也能从逻辑备份恢复数据,不至于全部推到重来。
2.2 检查 Docker 运行状态和系统基线
登记完成后,要花几分钟确认宿主机当前状态,尤其是下面这几项:
docker --version docker info | grep -E "Server Version|Storage Driver|Cgroup Driver|Docker Root Dir" sudo systemctl status docker --no-pager sudo df -h sudo apt-mark showhold这里我特别关注两个输出项:存储驱动和 Cgroup 驱动。
存储驱动必须是 overlay2。Ubuntu 20.04 的默认内核基本都支持 overlay2,大多数用户也是这个驱动。但如果你是从很老的机器升级上来的,docker info里显示 vfs 或者 devicemapper,那就要警惕了。devicemapper 在新版本 Docker 里已经完全不支持,daemon 直接起不来,这个问题我在 3.2 节会详细讲。
Cgroup 驱动建议是 systemd。如果你看到 cgroupfs,也不要急着改,先确认当前容器是否正常运行。升级过程中改 cgroup 驱动属于额外动作,如果没有 K8s 之类的强依赖,可以暂时不动,避免引入新的变量。
2.3 确认 Docker APT 源能正常更新,别在入口就被卡住
Ubuntu 上 Docker 升级依赖 APT 源,这一步如果没配对,后面全是白搭。最常见的现象是执行sudo apt update的时候报 404 或者签名错误,原因是源文件里的仓库地址已经失效,或者 GPG key 不对。
我现在的习惯是,升级前先跑sudo apt update,看输出里有没有 docker 相关的报错。如果发现源有问题,不要反复重试老地址,直接重新配置一条可靠的源。
当前我在 Ubuntu 20.04 上最常用的配置方式如下:
# 以阿里云 Docker CE 镜像源为例 curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu focal stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt update配置好之后,务必用apt-cache policy docker-ce确认能拉到官方同源的 docker-ce 包。看到输出里有 20.10、24.x、26.x 这类版本号,说明源没问题,可以放心进入下一步。
3. 核心升级实操:从 apt 命令到 daemon 启动
3.1 执行升级的命令和参数选择
准备工作和源都确认好后,执行升级本身反而很简单。我一直用的命令是:
sudo apt install --only-upgrade docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin这里有两个细节需要说明。
第一,我特意没有用apt upgrade做全局升级,而是用--only-upgrade限定只升级 Docker 相关组件。生产环境里,系统里可能还有其他软件依赖特定版本的内核库、运行库,全局升级容易把不相关的东西一起动,排查问题时很难定位。
第二,升级 docker-ce 的同时,必须把 docker-ce-cli、containerd.io、docker-buildx-plugin、docker-compose-plugin 一起带上。很多人只装 docker-ce,结果版本升级后 client 还是老版本,或者 compose 插件缺失,用起来各种别扭。
如果你想升级到指定版本,比如选择 26.x 稳定版,先查可用的版本列表:
apt-cache madison docker-ce输出类似这样:
docker-ce | 5:26.1.4-1~ubuntu.20.04~focal | https://mirrors.aliyun.com/docker-ce/linux/ubuntu focal/stable amd64 Packages然后安装时加上版本号:
sudo apt install docker-ce=5:26.1.4-1~ubuntu.20.04~focal docker-ce-cli=5:26.1.4-1~ubuntu.20.04~focal containerd.io=1.7.18-1生产环境我一般不建议追最新版本,选一个发布超过 1 到 2 个月的稳定小版本比较稳妥,让社区把新版本的坑先趟平。
3.2 daemon.json 的兼容性整理,这是最容易翻车的环节
升级包安装完成后,系统会自动尝试重启 docker daemon。如果重启失败,最可能的原因就在/etc/docker/daemon.json里。
升级前一定要先看这个文件:
cat /etc/docker/daemon.json我见过最典型的致命配置是"storage-driver": "devicemapper"。这通常是早期用 Ubuntu 16.04 搭建 Docker 环境时留下的配置,后来系统一路升级到 20.04,daemon.json 一直没动过。新版本 Docker 不支持 devicemapper,dockerd 会直接拒绝启动,日志里会提示类似devicemapper was deprecated或unsupported storage driver。
遇到这种情况,需要把存储驱动改成 overlay2:
{ "storage-driver": "overlay2", "exec-opts": ["native.cgroupdriver=systemd"] }这里有个必须要提醒的坑:修改存储驱动是一个大动作,涉及底层存储层重建,旧镜像可能无法直接读取。如果升级后 daemon 起不来,你要先判断是不是这个原因。确认后,改成 overlay2 再启动,如果某些镜像丢失,从 image_list 文件里找回来重新拉取,数据卷部分不受影响,容器数据还在。
除了存储驱动,还有三类配置升级时不能乱动:
live-restore必须保留 true,这是无缝升级的关键;>sudo systemctl daemon-reload sudo systemctl restart docker sudo journalctl -u docker -n 50 --no-pager日志里如果出现
failed to start containerd或者context deadline exceeded,优先检查 containerd 的版本。3.3 升级后一定要看的几个版本与系统信息
升级完成不代表万事大吉,我用下面的命令做快速判断:
docker version docker info | grep -E "Server Version|Storage Driver|Cgroup Driver|Live Restore" docker compose version docker buildx version这里有三个重点。
第一,确认 Server 版本已经升级到目标版本。很多人只盯着 Client 版本,实际上真正干活的是 Server 端 daemon。
第二,确认 Storage Driver 是 overlay2、Live Restore 是 true。
docker info输出里如果显示 Daemon 配置和预期不一致,先回 daemon.json 里查。第三,docker compose 插件要单独确认。新版本 Docker 使用的是
docker compose命令(中间有空格),和老版本独立的docker-compose不是一回事。如果你之前安装过 docker-compose-plugin,升级后会同步更新;如果没有,要单独安装:sudo apt install docker-compose-plugin补充一点:升级后你可能会注意到 compose 项目里的
version: "3"字段开始出现提示信息。这是 Compose V2 的正常反应,老项目写法基本兼容,但如果项目里用了特别老的扩展字段,建议用docker compose config先解析一遍,语法问题当场就能看出来。4. 升级后的验证清单:容器、网络、业务三关
4.1 容器状态检查与启动策略
升级后第一次执行
docker ps -a,看到容器全是 Exited 状态很正常,不要慌。重点在于确认每个容器的重启策略是否在升级后正常生效。docker inspect -f '{{.Name}} {{.HostConfig.RestartPolicy.Name}}' $(docker ps -aq) | sort如果容器配置了
unless-stopped或always,docker daemon 重启后理论上会自动拉起。但实际环境中,容器可能因为端口占用、挂载目录权限变化等原因启动失败。我的习惯是按依赖顺序手动启动:docker start mysql8 redis gitlab nginx然后逐个看日志,确认容器内部进程真的起来了:
docker logs --tail 50 mysql8 docker logs --tail 50 gitlab这里特别提醒:像 GitLab 这种体量大的容器,即使状态变成 running,内部服务也可能还没完全就绪。一定要看健康检查状态和实际业务响应,不要看到容器在跑就宣布胜利。
4.2 网络、存储和镜像源的实际验证
容器能起来,不等于网络一定通。docker daemon 升级时会重建网桥和部分 iptables 规则,这个过程中最容易出现端口映射失效、容器间互通异常的问题。
我的验证套路是:
# 1. 验证本机端口映射 ss -tlnp | grep 容器映射端口 # 2. 进入容器测试 DNS 解析 docker exec nginx ping -c 2 registry.example.com # 3. 自定义网络内容器互通 docker network inspect 网络名称如果你用的是自定义 bridge 网络,升级后网络对象通常还在,连接的容器也还在,但子网和网关信息可能发生细微变化。应用代码里如果写死了容器 IP,升级后很可能出现“连不上数据库”“访问不到服务”这种诡异故障。
镜像源验证我建议直接拉一个测试镜像:
docker pull hello-world如果有超时或者速度特别慢,优先怀疑 registry-mirrors 失效。现在的可用镜像地址变化很快,跟上一次升级时用的不一定一致,确认无法使用后,换成可用地址,重启 docker 再试。
存储这块,查看数据卷是否都完整挂载:
docker ps -q | xargs -I {} docker inspect {} --format '{{.Name}} {{range .Mounts}}{{.Source}}->{{.Destination}} {{end}}'确认挂载源路径还在,目录内容没有丢失。
4.3 业务服务验证,别只看容器是 running
最后一步是真实业务验证。命令行层面的健康检查忽略了很多业务细节,所以我通常这样测:
- Web 服务:curl 首页和关键接口,看状态码和响应耗时;
- 数据库:执行一条最简单的查询或事务,确认连接正常;
- 消息队列:发一条测试消息并消费,确认链路通;
- 定时任务:确认下一次触发能正常执行。
这里没有统一脚本,核心思路是“最小验证 + 关键路径”。我在一次升级完以为一切正常,结果客户反馈接口偶发超时,排查半天发现是容器重建后 IP 变了,而在另一个服务的配置文件里还在用旧 IP。从那以后,只要涉及跨容器调用,我验证时候一定会走一遍完整业务调用链,绝不只盯“容器活着”。
5. 升级实录:最常踩的 6 个坑与排查经验
5.1 常见问题速查表
现象 可能原因 处理方式 apt update 报 404 或仓库失效 Docker 源地址或签名过期 重新配置 docker.list,换成可用镜像源 升级后 docker daemon 启动失败 daemon.json 里有废弃配置 逐行检查配置,storage-driver 改为 overlay2 dockerd 报 failed to connect to containerd containerd.io 未一起升级或版本不匹配 检查 apt-mark showhold,解除锁定后重新安装 containerd.io 容器全部 Exited 且不会自动拉起 restart 策略缺失或 daemon 重启时序问题 手动 start,并检查 RestartPolicy 配置 端口映射失效,外部无法访问 docker0 网桥重建、iptables 规则冲突 用 iptables -t nat -L -n 检查 DOCKER 链,调整 ufw 规则 拉取镜像超时失败 registry-mirrors 地址失效 换成可用镜像地址,重启 docker 后重试 5.2 一个印象深刻的排查案例
之前帮一台机器升级,升级本身很顺利,docker version 也正常,但我发现 GitLab 的页面突然打不开。ss 看端口都在监听,curl 本机 80 也正常,但从外部访问就是不通。排查到最后,发现是这台机器之前手动改过 ufw 规则,Docker 升级时重建了 iptables 的 DOCKER 链,NAT 规则和 ufw 规则叠加后出现了冲突。
这个案例说明一个关键点:升级后如果遇到“本机能通、外部不通”,不要一头扎进容器日志里,先站在宿主机网络层面排查。
处理方式是调整 ufw 放行规则,让 Docker 映射出来的端口在防火墙层放通,或者把该容器改为 host 网络模式来规避 NAT 链的问题。具体选哪个要看业务需求,我的习惯是优先调整 ufw 规则,尽量不动容器网络模式。
5.3 升级后的日常维护,为下次省点事
升级这件事不是一锤子买卖。我现在的固定习惯有三条:
第一,每次升级前把容器清单和配置备份成一个固定脚本,放在项目目录里,下次直接执行,不用临时想命令。
#!/bin/bash # docker_pre_upgrade_backup.sh docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" > container_list_$(date +%F).txt docker image ls --format "table {{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}" > image_list_$(date +%F).txt docker volume ls > volume_list_$(date +%F).txt第二,升级完成后,把 docker version、docker info 的关键输出、验证结果记录到一个 Markdown 文件里,作为每次升级的存档。下次出问题,翻历史记录能很快定位是什么时候发生的变更。
第三,关注 Docker 在 Ubuntu 20.04 上的支持周期。Launched 版本有自己的生命周期,Docker 官方也会对旧版系统保留一段时间的兼容支持。如果机器上还跑着 ROS、SLAM 仿真这类依赖显卡透传的环境,升级 Docker 后务必用
nvidia-smi和容器内 GPU 验证命令双重确认,这类环境配置一次很花时间,别让升级把好不容易配好的 GPU 能力弄丢。回到开头那句话:升级 Docker 不可怕,可怕的是不准备就动手。把风险清单过一遍,把配置备份做一遍,把验证流程走一遍,整个升级就是一次可预测的常规操作。每次升级完我都会额外记录一句关键信息到运维笔记里,比如“这次升级后 bridge 网络的子网段变化了”“这次 registry mirror 换成了某个新地址”,这些小记录在后续排障时真的帮我节约过不少时间。希望这篇实战记录对你有用,也希望你的升级过程比我的更顺利。