一、升级之前先搞清楚"升的是什么"
Debian 的大版本升级(比如从 11 到 12)看起来只是换一个软件源,实际是几千个软件包同时换版本、一批配置文件格式变更、若干旧组件被移除。它不是"更新"的加强版,而是一次系统级的迁移。所以第一件事不是敲命令,而是弄清楚风险和回退路径。
我自己的判断标准很简单:如果这台机器上跑着不能停的服务,且没有可用的快照,就不要在同一个晚上既做升级又做迁移。升级应该在"能回滚"的前提下做,而回滚最可靠的方式是在虚拟化层或云厂商那边留一个快照,不是靠 apt 自己回退。Debian 官方明确说过不支持降级,一旦升上去,退回旧版本基本等于重装。
这篇文章按实际操作顺序写:准备、预检、执行、验证、收尾,每一步都给出可执行的命令和常见卡点。
二、准备:备份与快照必须先落地
准备阶段有三件事,顺序不能颠倒。第一件是快照。如果是云主机,直接在控制台做整机快照;如果是物理机,至少保证有一套完整备份在别处。第二件是记录当前状态,升级完要拿它做对照:
dpkg --get-selections > /root/pkg-before.txt
apt-mark showmanual > /root/manual-before.txt
systemctl list-unit-files --state=enabled > /root/units-before.txt
nginx -T > /root/nginx-before.conf 2>/dev/null
mysqldump --single-transaction --all-databases > /root/db-before.sql这几份文件在事后排查"升级后少了什么服务"时极其有用。特别是 apt-mark showmanual,它记录了你手动装过的包——升级过程中如果有包被标记成"可自动移除",事后可以用这份清单核对。
第三件事是清理和修补现有系统,让升级过程的变量少一点:
apt update && apt full-upgrade -y
apt --fix-broken install
apt autoremove --purge一定要在旧版本上把 held 的包处理掉。用 apt-mark showhold 看看有没有被锁版本的包,这些包在升级后会阻止整个流程,表现为 apt full-upgrade 一直提示"以下软件包将被保留"。要么解除 hold,要么明确记下并接受它不升级。
三、预检:把"已知不兼容"提前找出来
预检的核心是把"升级后才发现"变成"升级前就知道"。先确认当前版本和目标的对应关系:
cat /etc/debian_version
lsb_release -a 2>/dev/null || cat /etc/os-release再看哪些包来自第三方源,第三方源是最常见的升级失败原因,因为它们的仓库往往没有对应新版本的构建:
grep -rn --include='*.list' --include='*.sources' -E '^deb ' /etc/apt/sources.list /etc/apt/sources.list.d/把输出里所有非 deb.debian.org / security.debian.org 的源列出来,逐个去官网确认它有没有新版本的仓库。PHP 的 sury 源、Nginx 官方源、Docker 官方源、NodeSource 是最常见的四个,它们的路径里通常带发行版代号(bullseye、bookworm),升级后必须同步改。没改的话,apt update 会报 404,或者更糟——静默地拿到旧版本的包。
还要检查 PHP 这类"版本随发行版走"的组件。Debian 12 默认带 PHP 8.2,如果站点代码只兼容 7.4,升级后可能直接白屏。预检阶段最省事的做法是先在一台测试机上跑一遍,如果没有测试机,至少把 php -v 和站点代码里用到的扩展列出来,逐个查兼容性。
另外记得看一眼磁盘空间。apt full-upgrade 需要下载几百兆到一两个 G 的包,/var/cache/apt 太小会中途失败,失败在处理 dpkg 阶段时是最难收拾的:
df -h / /var /var/cache/apt
du -sh /var/cache/apt/archives四、执行:把软件源换掉再升级
Debian 的软件源现在有两种格式:传统的单行 sources.list,和 Debian 12 引入的 deb822 格式(.sources 文件)。先确认你用的是哪种,别照着旧教程改错文件。
传统格式的改法是整体替换代号,把 bullseye 换成 bookworm:
sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list
sed -i 's/bullseye-security/bookworm-security/g' /etc/apt/sources.list
apt update注意官方在 Debian 12 里把安全仓库的路径从 bullseye/updates 改成了 bookworm-security,只做一次全局替换可能留下旧路径,所以要单独确认。检查结果:
apt-cache policy | grep -c bookworm
apt update 2>&1 | grep -Ei 'err|404|warn'apt update 的输出必须干净。如果出现 404 Not Found,说明某个源路径还没改对;如果出现 GPG 错误,说明签名密钥过期了,需要先更新密钥再继续。这一步不干净就绝对不要往下走。
然后是正式升级,分两段做。先做最小升级,把升级工具本身更新到新版本:
apt upgrade --without-new-pkgs -y再做完整升级,这一步才会真正处理包之间的依赖变更、删除冲突包:
apt full-upgrade -y这里会弹出一堆配置文件的询问界面,也就是 conffile 冲突。默认选项是"保留当前版本",对生产环境的配置文件(Nginx、PHP、SSH)通常应该保留,因为新的默认配置会覆盖掉你所有的自定义。但 /etc/ssh/sshd_config 这类安全相关的文件,如果新版本改了默认值,保留旧的可能留下隐患,所以事后要单独 diff 一遍。
一个实用的技巧是升级前把所有 /etc 下的配置文件打个快照,升级后统一看哪些变了:
tar czf /root/etc-before.tar.gz /etc
# 升级后
diff -rq /root/etc-snapshot /etc 2>/dev/null | head -50如果 apt full-upgrade 中途失败(比如断电、磁盘满),先不要重启,按提示修复:
dpkg --configure -a
apt --fix-broken install这两条命令能修掉绝大多数"半升级"状态。在 dpkg --configure -a 成功之前千万不要重启,半配置状态下的系统很可能起不来。
五、重启之后:按顺序验证
内核和 systemd 都换了版本,必须重启。重启前先把能在重启前做的检查做完:
dpkg -l | grep -E '^i[^i]' | head -20 # 非完全安装状态的包,应该为空
ls /etc/*.dpkg-dist /etc/*.dpkg-new 2>/dev/null第一条应该没有输出(或者只有极少数可解释的包)。第二条列出的 .dpkg-dist 是"新版本配置放在这里、旧配置仍生效"的文件,需要人工合并,别直接删。
重启后按"从底层到上层"的顺序验证:
- 系统层:
systemctl --failed应该为空,dmesg -l err看有无硬件/驱动报错; - 网络层:
ip a确认网卡命名没变(Debian 12 沿用了可预测网卡名,但云主机的网卡名有可能从ens3变成ens5,导致静态 IP 配置失效); - 服务层:逐个检查 Nginx、PHP-FPM、MySQL、Redis、cron 的状态和监听端口;
- 应用层:访问站点首页、登录后台、发一篇测试文章、上传一张图。
网卡命名变化是升级后"机器能 ping 通但连不上"的典型原因。如果发现网络配置里的网卡名对不上,改 /etc/network/interfaces 或者 systemd-networkd 的 .network 文件即可,不用重装。查实际名字用 ip -br link。
六、收尾:清理旧内核与第三方源
升级完成后,系统里通常留着上一个版本的内核和一堆不再需要的包。清理要谨慎,至少留一个能启动的内核:
dpkg -l | grep linux-image
apt autoremove --purge
apt autocleanapt autoremove 会移除标记为自动安装且不再被依赖的包,正常情况下它会保留当前内核和最新一个旧内核。不要手动 dpkg --purge 掉所有旧内核——如果新内核在某个硬件上起不来,旧内核就是你唯一的救生艇。
然后是第三方源。逐个把路径里的旧代号改掉,或者在新版本没有对应构建时先禁用:
sed -i 's/bullseye/bookworm/g' /etc/apt/sources.list.d/*.list
apt update && apt full-upgrade -yPHP 这类组件如果之前用的是第三方源,升级后可能同时存在两个版本的 PHP。用 update-alternatives --config php 和 php -v 确认当前生效的版本,并检查 PHP-FPM 的 socket 路径有没有变——Nginx 的 fastcgi_pass 指向旧版本 socket 是升级后 502 的最常见原因:
ls /run/php/
grep -rn 'fastcgi_pass' /etc/nginx/ | head七、几条踩过坑的经验
第一,不要在业务高峰期做,也不要在周五晚上做。升级过程中如果遇到 conffile 冲突需要人判断,你必须清醒地在场。
第二,先把 SSH 连接保成两条。升级过程重启 sshd 时如果配置有问题,另一条已经建立的连接还能让你进去修。更好的做法是同时开一个云厂商的 VNC 控制台,那是网络层挂掉时唯一的入口。
第三,把升级当作一次"文档更新"的机会。升级过程中发现的每一个卡点——哪个源要改、哪个包要 hold、哪个配置文件不能覆盖——都记下来,下次升级时这份记录能省掉八成时间。
第四,小版本升级(比如 12.5 到 12.7)走正常的 apt upgrade 就行,不需要这一整套流程。只有跨大版本才值得这么谨慎。
最后一句:升级的价值不在"版本号更新",而在于能继续拿到安全补丁。当一个发行版进入 LTS 末期,你能做的选择只会越来越少。所以与其拖到不得不升,不如在时间宽裕的时候主动升一次,把流程跑熟。