
很多人第一次在 Ubuntu 里看到 .deb 安装包下意识的反应就是双击或者sudo dpkg -i xxx.deb但轮到自己手里只有一坨源代码、脚本或者编译好的二进制文件时就不知道怎么分发给别人了。其实在 Ubuntu 上制作 deb 安装包并不是什么高深技能只要理解了 deb 包的目录结构和control文件你完全可以手搓一个干净、好卸载、还能带维护脚本的安装包。不管是给团队内部做一个工具分发还是给开源项目打一个“像模像样”的发布包这招都很实用。这篇文章我会从零开始把一个 Shell 脚本打包成可安装、可卸载的 deb 包再讲清楚底层原理和高级打包工具最后把常见坑都列一遍。1. 为什么要把程序做成 deb 安装包1.1 deb 包到底是个什么东西deb 是 Debian/Ubuntu 系列系统使用的软件包格式。它本质上是 Unix 下最常见的ar归档文件里面装了三个核心部分debian-binary、control.tar.*和data.tar.*。你可以用ar t直接拆开看$ ar t hello-deb_1.0_amd64.deb debian-binary control.tar.xz data.tar.xzdebian-binary里面只有一行2.0表示包格式版本。control.tar.xz里放的才是“安装说明书”包含control、md5sums、维护脚本等。data.tar.xz就是真实要安装到系统里的文件比如usr/bin/hello-deb、etc/hello-deb/config.conf这些。dpkg在安装时先读控制信息再把数据解压到根目录最后执行维护脚本完成配置。搞清楚这一点之后再遇到“我怎么把程序发给别人”这类问题思路就清楚了deb 包就是在打包一个模拟系统根目录的树把文件按绝对路径放好再写一份元数据。1.2 用 deb 而不是压缩包分发的好处很多人在 Linux 上分发工具图省事直接丢一个tar.gz加一个install.sh。但这么做有几个很现实的问题用户执行完脚本之后系统里到底多了哪些文件连你自己都不一定记得住出了问题想卸载只能靠脚本里的反安装逻辑逻辑一旦漏掉就会残留垃圾。deb 包把这些事全部标准化了。表格对比一下能力tar.gz install.shdeb 包卸载依赖脚本记性没有标准dpkg remove、apt remove一键清理依赖关系人工检查容易漏Depends字段自动处理文件跟踪无记录dpkg -L查看包安装的所有文件安装前/后动作脚本硬编码preinst、postinst、prerm、postrm标准钩子版本升级手动覆盖apt upgrade统一管理实际项目中最典型的场景就是给内部小团队发命令行工具。直接丢一个编译好的二进制让同事扔到/usr/local/bin看起来简单但等工具更新到第 3 个版本时同事机器上可能还跑着旧版本连他自己都不知道。打成 deb 包之后走apt install ./xx.deb、apt upgrade就能统一更新省心很多。1.3 理解 deb 包的路径映射deb 包里面是不带根目录的打包时构建目录里的路径会直接对应到系统根目录。比如构建目录下有一个usr/bin/hello安装后实际路径就是/usr/bin/hello。而DEBIAN/这个目录比较特殊它不是要安装到系统的文件而是存放control、维护脚本等元数据的地方。打个比方deb 包就像一个搬家公司的清单DEBIAN是“搬运说明”data部分是“家具和行李”。搬运工按照“搬运说明”把“行李”放到指定房间而不是把“搬运说明”本身也搬进去。所以打包时千万不要把源码目录、构建缓存这些乱七八糟的东西放到包里面只保留从根目录开始映射的相对路径即可。2. 打包工作的准备与环境配置2.1 需要安装哪些工具打包最核心的工具dpkg-deb在 Ubuntu 上是自带的不需要额外安装。dpkg-deb负责构建、查看 deb 包功能很全。如果你要做源码编译、打包的前置工作那么再装一个build-essential就够了sudo apt update sudo apt install -y build-essential后面要介绍的checkinstall和fpm属于进阶工具不是必需的。如果你只是临时手动打一两个包完全不用装那么多东西用系统自带的dpkg-deb就够。日常我在 Ubuntu 22.04/24.04 上都是这套组合稳定得一批。2.2 创建一个简单的示例项目为了把整个过程讲明白我设计了一个非常简单的命令行工具hello-deb。它会被安装到/opt/hello-deb/目录然后通过维护脚本在/usr/bin/下创建一个软链接用户可以直接敲hello-deb调用。工具本身是一个 Shell 脚本读取一个配置文件后输出问候语。先创建构建目录mkdir -p ~/hello-deb/hello-deb-1.0 cd ~/hello-deb/hello-deb-1.0 mkdir -p DEBIAN opt/hello-deb etc/hello-deb然后写脚本cat opt/hello-deb/hello-deb.sh EOF #!/bin/bash VERSION1.0 CONF_FILE/etc/hello-deb/config.conf if [[ -f $CONF_FILE ]]; then source $CONF_FILE fi echo Hello from deb package ${VERSION}, name${NAME:-world} EOF chmod 755 opt/hello-deb/hello-deb.sh再写一个配置文件echo NAMEUbuntu etc/hello-deb/config.conf chmod 644 etc/hello-deb/config.conf这里刻意把脚本放到/opt而不是直接放/usr/bin是为了演示维护脚本的作用。系统里如果有冲突直接覆盖/usr/bin下的文件很危险放到/opt再用软链暴露给用户既干净又方便管理。2.3 目录结构约定与 FHS 简述Linux 目录结构是有行业约定FHSFilesystem Hierarchy Standard的deb 包应该尽量遵守。简单来说/usr/bin用户命令几乎所有用户都能执行。/usr/lib库文件和应用的数据文件。/etc系统配置文件。/opt第三方独立软件适合一个软件自带一堆资源文件的情况。/var/lib运行时产生的数据比如数据库、缓存。如果你的应用是一个二进制加上一堆资源、配置文件我会建议把资源放到/opt/应用名/下把全局配置放到/etc/应用名/下二进制本体放到/usr/bin或者用软链暴露。不太建议直接往/usr/local里塞/usr/local通常是给系统管理员本地装东西用的由手动编译安装的文件占据deb 包和它混在一起会让用户困惑。3. 手把手实现一个可安装、可卸载的 deb 包3.1 control 文件是包的“身份证”deb 包的control文件决定了这个包叫什么名字、什么版本、需要什么依赖、谁来维护。我先写一份最基础但完整的版本Package: hello-deb Version: 1.0 Architecture: amd64 Maintainer: Your Name youexample.com Depends: bash Section: utils Priority: optional Description: A simple hello world deb package This is my first deb package made by hand. It installs a script under /opt and creates a symlink in /usr/bin.把这个文件用cat写进DEBIAN/controlcat DEBIAN/control EOF Package: hello-deb Version: 1.0 Architecture: amd64 Maintainer: Your Name youexample.com Depends: bash Section: utils Priority: optional Description: A simple hello world deb package This is my first deb package made by hand. It installs a script under /opt and creates a symlink in /usr/bin. EOF字段的含义Package包名也是卸载时用的名字必须全小写不能用下划线。Version版本号推荐用1.0.0这种格式避免乱加连字符。Architecture架构二进制包写amd64纯脚本、纯 Python 项目可以写all。Maintainer维护者信息至少要填姓名和邮箱。Depends运行时依赖多个依赖用逗号分隔可以带版本要求比如bash ( 4.0)。Section和Priority软件分类和优先级utils、optional是常见组合。Description有个很容易踩的坑第一行是短描述后面可以写多行长描述但长描述每一行开头必须有一个空格否则dpkg-deb会报格式错误。别问我怎么知道的。3.2 添加维护脚本 postinst、prermpostinst是安装完成后执行的后置脚本prerm是移除之前执行的前置脚本。这两个脚本是 deb 包区别于普通压缩包的关键能力。我给这个示例添加两个脚本。安装后创建软链接cat DEBIAN/postinst EOF #!/bin/sh set -e ln -sf /opt/hello-deb/hello-deb.sh /usr/bin/hello-deb echo hello-deb installed: run hello-deb EOF chmod 755 DEBIAN/postinst卸载前删除软链接cat DEBIAN/prerm EOF #!/bin/sh set -e if [ -L /usr/bin/hello-deb ]; then rm -f /usr/bin/hello-deb fi exit 0 EOF chmod 755 DEBIAN/prerm维护脚本有一个非常重要的要求必须有可执行权限。如果postinst没有执行权限dpkg会直接忽略它执行顺序的意义就丢了。另外脚本里用set -e是一个好习惯一旦某一步出错就立即退出避免留下半安装状态。3.3 用 dpkg-deb 生成 deb 包目录和脚本都准备好后回到上一级目录执行打包命令cd ~/hello-deb dpkg-deb --build --root-owner-group hello-deb-1.0 hello-deb_1.0_amd64.deb--root-owner-group参数非常关键。如果不加包内文件的属主会是你当前的普通用户而不是 root。安装到系统时dpkg会按照归档里的 uid/gid 还原文件属主导致系统文件的属主变成一个不存在的普通用户这显然是不对的。加了--root-owner-group之后所有文件属主强制变成root/root。如果你的dpkg-deb版本比较老不支持这个参数可以用fakeroot dpkg-deb --build ...达到同样效果。生成后先验证再安装dpkg-deb --info hello-deb_1.0_amd64.deb dpkg-deb --contents hello-deb_1.0_amd64.deb--info输出 control 信息--contents列出包内所有文件。这两条命令应该成为你的习惯发布前至少跑一遍。3.4 安装、运行、卸载全过程安装 deb 包推荐用apt而不是裸的dpkg -i因为apt会帮你处理依赖关系。注意本地文件一定要带./路径否则apt会认为你想从软件源里安装sudo apt install ./hello-deb_1.0_amd64.deb安装完成后运行hello-deb正常会输出Hello from deb package 1.0, nameUbuntu用dpkg -L可以查看这个包安装的所有文件dpkg -L hello-deb卸载sudo apt remove hello-deb卸载后检查软链接是否被清理ls -l /usr/bin/hello-deb这个包虽然简单但从 “安装 → 运行 → 卸载” 已经形成了一个完整闭环完全符合分发要求。3.5 包内容正确性与文件校验deb 包安装时dpkg会利用md5sums文件做完整性校验。如果打好的包你希望安装后能看到文件校验信息可以手动生成DEBIAN/md5sumscd hello-deb-1.0 find . -path ./DEBIAN -prune -o -type f -print0 | xargs -0 md5sum DEBIAN/md5sums sed -i s# ./# /# DEBIAN/md5sums注意把./opt的前缀改成/opt这样md5sums中的路径就和系统根目录对应了。这一步不是必须的但很多长期维护的仓库会要求包含这个文件。生成后重新dpkg-deb --build即可。4. 让打包更高效checkinstall 与 fpm4.1 checkinstall编译安装一步转包很多开源软件走的都是./configure make make install这条路。每次都要手动打包有点累checkinstall可以拦截make install过程中对文件系统的操作自动生成一个 deb 包安装到系统里。用起来很简单sudo apt install -y checkinstall ./configure make sudo checkinstall -D make install执行过程中它会问你包名、版本、描述、依赖等按回车接受默认值即可。-D表示生成 Debian 包。我在自己电脑上编译一些不在官方源里的小工具时很喜欢用这种方式方便以后apt remove清理。但要注意checkinstall不适合自动化流水线因为它是交互式的而且make install可能会把文件安装到/usr/local这类目录生成的包结构有时候不太规整。它更适合个人临时自用不适合做正式的发布物。4.2 fpm一条命令批量打包 deb如果你需要频繁打包或者想写脚本自动化发布fpm是目前最顺手的高效工具。它用 Ruby 写成支持多种源目录和多种目标包格式。安装方式sudo apt install -y ruby ruby-dev build-essential sudo gem install fpm在 Ubuntu 22.04 上Ruby 3.0 版本装 fpm 是没问题的。打包时假设你在/tmp/pkg-root下已经放好了usr/bin/myapp执行fpm -s dir -t deb -n myapp -v 1.0.0 -a all -C /tmp/pkg-root usr/bin参数含义-s dir源类型是普通的目录。-t deb目标格式是 deb。-n myapp包名。-v 1.0.0版本号。-a all架构all表示与架构无关。-C /tmp/pkg-root打包前先进入这个目录这样usr/bin在包内就是usr/bin。fpm 还能自动帮你搞定md5sums省去很多手动工作。我个人经验是临时快速分发用 fpm 足够但如果你想做精细化控制自定义维护脚本、依赖版本、多架构、发布到内部源还是手动写DEBIAN/目录更可靠。4.3 如何处理依赖关系和 Architecture依赖关系是 deb 包里最容易出问题的部分。Depends字段可以写多个依赖Depends: bash ( 4.0), libc6 ( 2.31), python3括号里的版本条件常见的有、、等。写依赖之前先确认目标环境是否满足。可以在打包前用apt depends ./你的包.deb检查当前系统会怎么解析这个依赖。还可以用dpkg -s 包名检查某个库版本。Architecture字段要注意区分纯脚本包写all编译好的 64 位二进制包写amd6432 位写i386ARM 机器写arm64。写错架构在apt install时会直接提示“包架构不匹配”而且比较难查。比如你在一台 x86 机器上编译了一个二进制却在 control 里写了all那你这个包其实放到 arm64 机器上是不能跑的这是概念错误。all只适用于不包含机器码的脚本、配置文件和数据文件。4.4 本地仓库与签名进阶拓展如果你需要给团队多人分发与其让他们单个apt install ./xxx.deb不如搭一个本地 apt 仓库把打好的 deb 包放进去团队直接apt update apt install hello-deb。制作仓库需要dpkg-scanpackages工具sudo apt install -y dpkg-dev dpkg-scanpackages -m . /dev/null | gzip -9c Packages.gz然后把整个仓库目录放到 Web 服务器或共享盘上客户端配置一个/etc/apt/sources.list.d/下的.list文件即可。这一步还可以配合 GPG 签名确保包来源可信。签名需要debsigs或dpkg-sig属于进阶玩法我建议先别急着上把基础打包流程跑通再说。5. 常见问题与排查技巧5.1 安装时提示依赖关系不满足错误示例dpkg: dependency problems prevent configuration of hello-deb hello-deb depends on libfoo ( 2.0); however: Package libfoo is not installed.这种情况最常见的原因就是Depends写高了、写错包名或者目标机器上确实缺这个库。临时补救可以用sudo apt install -f但这是“打补丁”根本解法是改control文件里的Depends字段重新打包。动手前用apt-cache show 包名查询实际可用的版本再决定写多大的版本约束。5.2 维护脚本没有执行或执行报错维护脚本没被执行九成是文件没有可执行权限。你可以在打包前检查ls -l DEBIAN/postinst权限必须是-rwxr-xr-x。如果不是就chmod 755 DEBIAN/postinst执行时报错最常见的原因是 Windows 下编辑脚本带了 CRLF 回车符。Linux 的/bin/sh遇到\r会直接报错。处理方式sed -i s/\r$// DEBIAN/postinst之后重新打包。安装出问题时还可以用sudo dpkg --configure -a重新触发所有未配置包的postinst脚本看具体报错信息。这个命令在处理半安装状态的包时很管用。5.3 装了但文件不知道去哪里如果你不确定包里的文件到底装到了哪里先看包内容dpkg-deb --contents hello-deb_1.0_amd64.deb已经安装之后再看系统里的实际路径dpkg -L hello-deb想反查一个系统文件属于哪个包dpkg -S /usr/bin/hello-deb这三个命令组合起来基本能把文件路径问题查个底朝天。5.4 文件名、版本号和大小写问题Debian 包的Package名要求小写字母加数字以及-和不允许下划线。文件名最好也遵循标准格式包名_版本号_架构.deb比如hello-deb_1.0_amd64.deb。版本号里不建议带空格、括号等特殊字符也不要乱用连字符。Debian 版本号中连字符是有特殊含义的上游版本号与 Debian 修订号分隔符如果写成Version: 1.0-1dpkg会把它解析成上游版本1.0、修订版本1。这本身合法但对新手来说容易造成预期混乱所以我建议统一用点号分隔比如1.0.0等需要发布修订版再升级成1.0.1。还有一个细节dpkg -i安装带下划线文件名的包通常没问题但apt install ./解析时可能会出问题所以规范命名能帮你避开很多奇奇怪怪的坑。5.5 打包后文件属主和权限不对发布前一定要看包内文件的属主和权限dpkg-deb -c hello-deb_1.0_amd64.deb正常情况应该是root/root权限根据文件类型来可执行文件755配置文件644目录755。如果显示的是你的用户名那说明打包时没有加--root-owner-group。目录权限尤其重要如果某个父目录权限是700普通用户就没法访问里面的文件安装后程序可能直接卡在“Permission denied”。这些坑我基本都在真实环境里踩过一遍尤其是 Windows 下编辑维护脚本踩得最深。现在我的习惯是每次打新包前先跑一遍dpkg-deb --info、dpkg-deb --contents再在一个干净的容器或虚拟机里apt install ./测一轮。这样做虽然多花几分钟但能拦下绝大多数发布事故。最后再分享一个小技巧如果你经常打包可以把手动目录结构保存成一个模板比如template/DEBIAN目录、template/usr/bin等每次建新包直接cp -r拷贝一份再改比从零搭建快很多。我个人在维护内部工具集时都是先手动打磨好一个包的完整结构和脚本后续新项目直接基于这个模板改再配一个简单的 shell 脚本自动化处理版本号整个打包流程就变得非常顺了。