Appearance
postinst 脚本完整解读与最佳实践

当你用 apt install myapp 安装一个软件包时,终端上滚动着一行行日志,最后提示"安装完成"。但你可能没想过:安装完成后,软件包是怎么完成"初始化"的?
答案是 postinst —— Debian 包维护者脚本中的"第一主角"。
本文是 Debian 打包专题的第一章,我们将深入解读 postinst 脚本的完整运行机制,涵盖所有触发场景,并给出生产级的最佳实践。
什么是 postinst?
postinst 是 Debian 软件包中的安装后执行脚本,在包的文件被解压到系统后运行。它的职责包括:
- 创建运行所需的系统用户
- 初始化配置文件(首次安装)
- 注册 systemd 服务(
enable+start) - 执行数据库迁移或初始化
- 触发其他服务的重启(如 Nginx 重新加载配置)
📌 命名规范:脚本位于
debian/目录下,打包后会被安装到/var/lib/dpkg/info/<package>.postinst。
postinst 的触发场景
很多新手以为 postinst 只在"安装"时运行一次。实际上,它在以下四种场景中都会被触发:
| 场景 | 第一个参数 | 说明 |
|---|---|---|
| 首次安装 | configure | 包第一次被安装到系统 |
| 版本升级 | configure | 从旧版本升级到新版本 |
| 版本回退 | abort-upgrade | 升级过程中失败,回退到旧版本 |
| 依赖问题 | abort-remove | 因依赖冲突导致卸载中止 |
其中 configure 是最常见的场景,但 abort-* 场景往往被忽略——而这恰恰是导致系统状态不一致的罪魁祸首。
postinst 的标准模板
基础模板(推荐)
bash
#!/bin/sh
set -e
case "$1" in
configure)
# 首次安装或升级时的初始化逻辑
;;
abort-upgrade)
# 升级失败回退时的清理
;;
abort-remove)
# 卸载中止时的恢复
;;
*)
echo "postinst called with unknown argument: $1" >&2
exit 1
;;
esac
# 通用处理:dh_installdeb 生成的片段会追加在这里
exit 0关键指令
set -e:任何命令失败立即退出,防止错误状态下继续执行case "$1":根据第一个参数执行不同分支
⚠️ 重要:
postinst脚本必须以exit 0结尾,否则 dpkg 会认为安装失败并回滚。
实战:为 Go 应用编写 postinst
假设我们有一个 Go 写的 Web 服务 myapp,需要完成以下初始化任务:
- 创建专用的系统用户
myapp - 注册 systemd 服务并启用
- 首次安装时生成默认配置文件
- 升级时保留用户已修改的配置文件
完整的 postinst 脚本
bash
#!/bin/sh
set -e
# 从 control 文件获取包名
PKGNAME="$DPKG_MAINTSCRIPT_PACKAGE"
case "$1" in
configure)
# 1. 创建系统用户(如果不存在)
if ! getent passwd myapp >/dev/null; then
adduser --system --group --no-create-home --home /var/lib/myapp myapp
fi
# 2. 创建数据目录并设置权限
mkdir -p /var/lib/myapp /var/log/myapp
chown myapp:myapp /var/lib/myapp /var/log/myapp
# 3. 处理配置文件(首次安装 vs 升级)
if [ -z "$2" ] || [ "$2" = "<old-version>" ]; then
# 首次安装:直接复制默认配置
cp /usr/share/myapp/config.example.yml /etc/myapp/config.yml
else
# 升级:保留现有配置,不覆盖
# 但如果配置文件不存在(可能是手动删除),则重新生成
if [ ! -f /etc/myapp/config.yml ]; then
cp /usr/share/myapp/config.example.yml /etc/myapp/config.yml
fi
fi
# 4. 注册并启用 systemd 服务(但不启动)
# 使用 deb-systemd-helper 而非直接 systemctl
deb-systemd-helper enable myapp.service >/dev/null
;;
abort-upgrade)
# 升级失败回退:什么都不做,系统会恢复到旧版本
# 但需要确保旧版本的服务状态正常
;;
abort-remove)
# 卸载被中止:重新启用服务
deb-systemd-helper enable myapp.service >/dev/null
;;
*)
echo "postinst called with unknown argument: $1" >&2
exit 1
;;
esac
# 调用 debhelper 自动生成的代码段
# 比如 dh_installdeb 会在这里追加 systemd 启动逻辑
if [ -x /usr/share/debconf/confmodule ]; then
. /usr/share/debconf/confmodule
fi
exit 0关键最佳实践
1. 使用 deb-systemd-helper 而非直接 systemctl
bash
# ❌ 错误:在 postinst 中直接调用 systemctl
systemctl enable myapp.service
systemctl start myapp.service
# ✅ 正确:使用 deb-systemd-helper
deb-systemd-helper enable myapp.service
deb-systemd-helper start myapp.service原因:
deb-systemd-helper会正确处理--no-restart-on-upgrade等策略- 支持
dpkg的--no-start选项(用户可禁止自动启动) - 避免在 chroot 或容器环境中报错
2. 区分首次安装和升级
bash
if [ -z "$2" ] || [ "$2" = "" ]; then
# 首次安装($2 为空)
else
# 升级($2 是旧版本号)
ficonfigure 的第二个参数是旧版本号:
- 首次安装:
$2为空 - 升级:
$2为旧版本号
3. 配置文件保留策略
原则:永远不要覆盖用户修改过的配置文件。
bash
# 策略1:仅首次安装时生成
if [ ! -f /etc/myapp/config.yml ]; then
cp /usr/share/myapp/config.yml.example /etc/myapp/config.yml
fi
# 策略2:使用 ucf(推荐,自动处理三方合并)
# 需要额外安装 ucf 包关于 ucf(User Configuration File)的完整用法,我们会单独写一篇进阶指南。
4. 不要在 postinst 中启动服务
bash
# ❌ 错误:在 postinst 中直接启动
systemctl start myapp.service
# ✅ 正确:仅启用,由 dh_installdeb 在安装完成后统一启动
deb-systemd-helper enable myapp.service原因:多个包同时安装时,过早启动服务可能导致依赖不满足。Debian 策略是将启动推迟到所有包安装完成之后。
常见陷阱与排坑
陷阱1:set -e 导致非致命错误中断安装
bash
# ❌ 错误:如果用户不存在,adduser 会报错退出
adduser --system myapp
# ✅ 正确:检查前置条件
if ! getent passwd myapp >/dev/null; then
adduser --system myapp
fi陷阱2:在 abort-upgrade 中执行破坏性操作
bash
# ❌ 错误:升级回退时删除了数据
abort-upgrade)
rm -rf /var/lib/myapp # 数据丢失!
;;
# ✅ 正确:abort-upgrade 什么都不做
abort-upgrade)
# 系统会自动回退到旧版本文件,无需额外操作
;;陷阱3:依赖其他未安装的包
bash
# ❌ 错误:postinst 中调用其他包的命令
configure)
mysql_install_db # 如果 mysql-server 未安装则失败
;;正确做法:将此类操作放到首次启动时执行,或在 control 中明确声明依赖。
postinst 的调用时序图
测试你的 postinst
模拟安装
bash
# 构建并安装包
dpkg-buildpackage -us -uc
sudo dpkg -i ../myapp_1.0.0_amd64.deb模拟升级
bash
# 安装旧版本
sudo dpkg -i myapp_0.9.0.deb
# 升级到新版本
sudo dpkg -i myapp_1.0.0.deb模拟回退
bash
# 升级到新版本(故意失败)
sudo dpkg -i myapp_1.0.0.deb || true
# 查看回退状态
sudo dpkg --get-selections | grep myapp查看 postinst 执行日志
bash
# dpkg 详细日志
sudo tail -f /var/log/dpkg.log
# 或使用
sudo dpkg --audit小结
| 要点 | 说明 |
|---|---|
postinst 在四种场景运行 | configure、abort-upgrade、abort-remove |
使用 deb-systemd-helper | 不要直接 systemctl |
| 区分首次安装与升级 | 通过 $2 判断旧版本 |
| 保留用户配置 | 不要覆盖 /etc/ 下的已有配置文件 |
| 不在 postinst 中启动服务 | 只 enable,由 dpkg 统一启动 |
| 测试所有场景 | 安装、升级、回退、卸载 |
下一步
在下一章中,我们将探讨 prerm(卸载前脚本)——它的触发场景比 postinst 更复杂,尤其是在处理依赖冲突时的 deconfigure 场景。
下一篇预告:第二章:prerm 脚本的作用与正确使用姿势
