Appearance
postrm 脚本的清理逻辑与 purge 陷阱

"apt install myapp 失败了,提示 'dpkg: error: package myapp is in a very bad inconsistent state'——我的系统废了!"
这是 postrm 脚本写错时最常见的后果:软件包处于半安装状态,无法重新安装,也无法卸载。
postrm(post-removal)是 Debian 包维护者脚本的"最后一关",负责在文件被删除后执行清理。它也是最危险的一关——因为这里的错误可能导致系统状态永久性损坏。
本文将深入解读 postrm 的运行机制,特别是 remove 和 purge 的本质区别,并给出生产级的安全实践。
postrm 是什么?
postrm 是 Debian 软件包中的卸载后执行脚本,在包的文件被从系统删除后运行。它的职责包括:
- 删除配置文件(
purge时) - 删除日志文件和数据目录(
purge时) - 删除系统用户(
purge时) - 清理
systemd残留(purge时) - 清理
update-alternatives注册(purge时) - 处理升级失败后的回滚
📌 命名规范:脚本位于
debian/目录下,打包后被安装到/var/lib/dpkg/info/<package>.postrm。
postrm 的触发场景
| 场景 | 第一个参数 | 第二个参数 | 说明 |
|---|---|---|---|
| 软件包卸载 | remove | 无 | 用户主动卸载(保留配置文件) |
| 彻底清除 | purge | 无 | 用户主动清除(删除所有配置) |
| 升级失败 | upgrade | 旧版本号 | 新版本安装失败,旧版本恢复 |
| 卸载后回滚 | failed-upgrade | 旧版本号 | 同 upgrade,但上下文不同 |
⚠️ 核心概念:
remove和purge是本质不同的操作。前者保留配置文件,后者彻底清除。
postrm 的标准模板
基础模板(推荐)
bash
#!/bin/sh
set -e
case "$1" in
remove)
# 用户卸载:不做任何清理
# 文件已删除,但配置保留
;;
purge)
# 彻底清除:删除所有配置文件、日志、用户
;;
upgrade)
# 升级失败回滚:清理新版本残留
;;
failed-upgrade)
# 卸载后回滚:清理新版本残留
;;
*)
echo "postrm called with unknown argument: $1" >&2
exit 1
;;
esac
exit 0实战:为 Go 应用编写 postrm
延续之前的 myapp Go 服务,我们需要:
remove时:什么都不做(保留配置)purge时:删除配置、日志、用户、systemd 残留
完整的 postrm 脚本
bash
#!/bin/sh
set -e
PKGNAME="$DPKG_MAINTSCRIPT_PACKAGE"
case "$1" in
remove)
# 用户卸载(保留配置):不做任何清理
# 让用户以后可以重新安装并恢复配置
;;
purge)
# ✅ 彻底清除:删除所有痕迹
# 1. 删除配置文件
rm -rf /etc/myapp
# 2. 删除日志文件
rm -rf /var/log/myapp
# 3. 删除数据目录
rm -rf /var/lib/myapp
# 4. 删除 systemd 残留
if [ -x /usr/bin/deb-systemd-helper ]; then
deb-systemd-helper purge myapp.service >/dev/null 2>&1 || true
fi
# 5. 删除系统用户
if getent passwd myapp >/dev/null 2>&1; then
deluser --system myapp >/dev/null 2>&1 || true
fi
# 6. 删除 update-alternatives 注册(如果使用)
if [ -x /usr/bin/update-alternatives ]; then
update-alternatives --remove myapp /usr/bin/myapp || true
fi
# 7. 清理可能残留的临时文件
rm -rf /tmp/myapp-* 2>/dev/null || true
;;
upgrade)
# 新版本安装失败,旧版本恢复
# 通常什么都不做,因为文件已经被旧版本覆盖了
;;
failed-upgrade)
# 同样:什么都不做
;;
*)
echo "postrm called with unknown argument: $1" >&2
exit 1
;;
esac
exit 0remove vs purge:本质区别
| 操作 | 删除文件 | 删除配置 | 删除用户 | 重新安装后 |
|---|---|---|---|---|
dpkg -r(remove) | ✅ | ❌ | ❌ | 配置保留,直接可用 |
dpkg -P(purge) | ✅ | ✅ | ✅ | 完全干净,重新初始化 |
用户视角
bash
# remove:保留配置
sudo apt remove myapp
# /etc/myapp/config.yml 还在
# 重新安装后,配置直接生效
sudo apt install myapp
# 服务使用原有配置启动 ✅
# purge:彻底清除
sudo apt purge myapp
# /etc/myapp/ 整个目录被删除
# 重新安装后,从默认配置开始
sudo apt install myapp
# 服务使用默认配置启动 ✅postrm 脚本视角
bash
remove)
# 文件已删除,但配置还在
# 我们什么都不做
;;
purge)
# 文件已删除,配置也要删除
# 我们清理一切
rm -rf /etc/myapp /var/log/myapp /var/lib/myapp
deluser myapp
;;关键最佳实践
1. remove 场景保持沉默
bash
# ✅ 正确:remove 什么都不做
remove)
# 保留所有配置和数据
;;
# ❌ 错误:remove 时删除了配置
remove)
rm -rf /etc/myapp # 用户无法保留配置!
;;2. purge 场景彻底清理 systemd
bash
# ✅ 正确:完整清理 systemd
purge)
if [ -x /usr/bin/deb-systemd-helper ]; then
deb-systemd-helper purge myapp.service >/dev/null 2>&1 || true
fi
# 手动清理可能残留的单元文件
rm -f /etc/systemd/system/multi-user.target.wants/myapp.service 2>/dev/null || true
rm -f /lib/systemd/system/myapp.service 2>/dev/null || true
systemctl daemon-reload >/dev/null 2>&1 || true
;;
# ❌ 错误:只 disable 不 purge
purge)
systemctl disable myapp.service # systemd 残留仍存在!
;;3. 善用 deb-systemd-helper purge
deb-systemd-helper purge 是专门为 postrm 设计的命令,它会:
- 删除所有 systemd 单元文件的链接
- 清理 systemd 的缓存状态
- 确保
systemctl status不再显示残留
bash
# 在 purge 场景中使用
purge)
deb-systemd-helper purge myapp.service
;;4. 删除用户时的安全检查
bash
# ✅ 正确:检查用户存在再删除
purge)
if getent passwd myapp >/dev/null 2>&1; then
deluser --system myapp >/dev/null 2>&1 || true
fi
;;
# ❌ 错误:直接删除(用户不存在时命令失败)
purge)
deluser myapp # 如果用户已删除,命令失败
;;5. 所有删除命令都要容错
bash
# ✅ 正确:目录不存在时也不报错
rm -rf /etc/myapp # rm -rf 天然容错(目录不存在不报错)
# ✅ 正确:删除用户时检查存在性
if getent passwd myapp; then
deluser myapp
fi
# ✅ 正确:使用 || true 容错
deb-systemd-helper purge myapp.service || true常见陷阱与排坑
陷阱1:remove 时删除了配置文件
bash
# ❌ 灾难性错误
remove)
rm -rf /etc/myapp # 用户无法保留配置了后果:用户执行 apt remove 后配置丢失,重新安装后需要重新配置。
陷阱2:purge 时忘记删除用户
bash
# ❌ 用户残留
purge)
rm -rf /etc/myapp
rm -rf /var/log/myapp
# 忘记删除用户 myapp后果:重新安装时 adduser 失败(用户已存在),导致安装中断。
陷阱3:systemd 残留未清理
bash
# ❌ systemd 残留
purge)
# 只删除文件,不清理 systemd
rm -rf /etc/myapp后果:systemctl status myapp 仍然显示服务信息,甚至 systemctl start myapp 会尝试启动已删除的服务。
陷阱4:postrm 失败导致包状态损坏
bash
# ❌ 错误:postrm 脚本失败
purge)
deluser myapp # 如果用户不存在,命令失败
rm -rf /etc/myapp # 这行不会执行
# 脚本以非 0 退出,dpkg 标记包状态为 "half-configured"后果:包无法重新安装,也无法卸载。系统提示 "package is in a very bad inconsistent state"。
修复"状态损坏"的方法
如果已经踩了陷阱4,可以用以下方法恢复:
bash
# 强制重新安装
sudo dpkg --force-depends -i myapp_1.0.0.deb
# 或强制移除(不执行脚本)
sudo dpkg --remove --force-remove-reinstreq myapp
# 或手动清除 dpkg 信息
sudo rm -rf /var/lib/dpkg/info/myapp.*
sudo dpkg --remove --force-remove-reinstreq myapppostrm 与其他脚本的协作
测试你的 postrm
模拟 remove(保留配置)
bash
# 安装包
sudo dpkg -i myapp_1.0.0.deb
# 检查配置文件是否存在
ls -la /etc/myapp/
# remove(触发 postrm remove)
sudo dpkg -r myapp
# 检查配置文件是否还在
ls -la /etc/myapp/ # 应该仍然存在 ✅模拟 purge(彻底清除)
bash
# 重新安装
sudo dpkg -i myapp_1.0.0.deb
# purge(触发 postrm purge)
sudo dpkg -P myapp
# 检查是否完全清理
ls -la /etc/myapp/ # 应该不存在 ❌
ls -la /var/log/myapp/ # 应该不存在 ❌
getent passwd myapp # 应该不存在 ❌
systemctl status myapp # 应该显示 "not found" ❌测试重新安装
bash
# purge 后重新安装
sudo dpkg -i myapp_1.0.0.deb
# 检查是否正常工作
systemctl status myapp # 应该正常运行 ✅查看 postrm 执行日志
bash
# dpkg 详细日志
sudo grep postrm /var/log/dpkg.logpostrm 安全清单
在发布你的 Debian 包之前,对照以下清单检查 postrm:
- [ ]
remove场景不做任何破坏性操作(保留配置、数据、用户) - [ ]
purge场景删除所有配置文件(/etc/下的所有文件) - [ ]
purge场景删除所有日志和数据目录 - [ ]
purge场景删除系统用户(检查存在性) - [ ]
purge场景清理 systemd 残留(deb-systemd-helper purge) - [ ]
purge场景清理update-alternatives(如果使用) - [ ] 所有删除命令都做了容错处理(
|| true或条件判断) - [ ] 脚本以
exit 0结尾 - [ ]
upgrade和failed-upgrade场景为空或只做无害操作 - [ ] 测试了 remove → install → purge → install 完整链路
小结
| 要点 | 说明 |
|---|---|
remove vs purge | remove 保留配置,purge 彻底清除 |
postrm remove | 什么都不做(保留用户数据) |
postrm purge | 删除配置、日志、用户、systemd 残留 |
使用 deb-systemd-helper purge | 专门清理 systemd 残留 |
| 所有命令都要容错 | 避免 postrm 失败导致包状态损坏 |
| 测试完整链路 | remove → install → purge → install |
维护者脚本系列总结
| 脚本 | 运行时机 | 核心职责 | 最大陷阱 |
|---|---|---|---|
| postinst | 安装后 | 初始化、启用服务 | 覆盖用户配置 |
| prerm | 卸载前 | 停止服务 | 删除用户数据 |
| postrm | 卸载后 | 清理残留 | 脚本失败导致状态损坏 |
这三个脚本共同构成了 Debian 包的完整生命周期管理。掌握它们,你就掌握了 Debian 打包的"灵魂"。
下一步
在下一章中,我们将进入实战打包阶段,把前面学到的维护者脚本知识应用到真实的 Golang 项目中。
