62 - 自定义系统打包与分发

当现成的软件包不能满足需求时,创建自定义包是自动化部署的基石。无论是将内部开发的软件打包成 .deb/.rpm,搭建私有 apt/yum 仓库供团队使用,还是构建 OCI 容器镜像进行分发——掌握打包技能让你不再受限于发行版官方仓库的边界。本章覆盖 Debian(.deb)、RHEL(.rpm)、Arch(PKGBUILD)三系的打包方法,以及私有仓库搭建和现代分发方案。


62.1 为什么创建自定义包

典型场景

场景说明
内部软件分发公司自研的后端服务,需要标准化安装
配置文件管理将应用配置打包为包,版本化管理
补丁/定制版本对上游软件打补丁后重新分发
系统模板预装一组工具的元包(metapackage)
合规要求统一软件版本和签名,通过审计
离线部署将软件和依赖打包,用于无网络环境

包管理 vs 手动部署

手动部署:scp → 手动改权限 → 手写 systemd unit → 没人知道装了什么
打包部署:dpkg -i app.deb → systemd 自动启用 → dpkg -l 看版本

62.2 Debian/Ubuntu:创建 .deb 包

最简方式:dpkg-deb

# 创建包的目录结构
mkdir -p myapp/DEBIAN
mkdir -p myapp/usr/local/bin
mkdir -p myapp/usr/lib/systemd/system
mkdir -p myapp/etc/myapp
 
# 放入你的程序文件
cat > myapp/usr/local/bin/myapp << 'EOF'
#!/bin/bash
echo "Hello from MyApp v1.0"
EOF
chmod 755 myapp/usr/local/bin/myapp
 
# 放入 systemd 服务文件
cat > myapp/usr/lib/systemd/system/myapp.service << 'EOF'
[Unit]
Description=MyApp Service
After=network.target
 
[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
Restart=on-failure
 
[Install]
WantedBy=multi-user.target
EOF

control 文件(包的元信息)

cat > myapp/DEBIAN/control << 'EOF'
Package: myapp
Version: 1.0.0-1
Section: utils
Priority: optional
Architecture: amd64
Depends: bash, systemd
Maintainer: Ops Team <ops@example.com>
Description: MyApp internal application
 This package contains the MyApp server application.
 It includes the binary, systemd service, and base
 configuration files.
EOF

维护脚本(optional)

# DEBIAN/preinst — 安装前执行
cat > myapp/DEBIAN/preinst << 'EOF'
#!/bin/bash
echo "Pre-install: preparing..."
# 如创建用户
if ! id -u myapp >/dev/null 2>&1; then
 useradd -r -s /usr/sbin/nologin myapp
fi
EOF
chmod 755 myapp/DEBIAN/preinst
 
# DEBIAN/postinst — 安装后执行
cat > myapp/DEBIAN/postinst << 'EOF'
#!/bin/bash
set -e
systemctl daemon-reload
systemctl enable myapp.service
systemctl start myapp.service || true
echo "MyApp installed and started"
EOF
chmod 755 myapp/DEBIAN/postinst
 
# DEBIAN/prerm — 卸载前执行
cat > myapp/DEBIAN/prerm << 'EOF'
#!/bin/bash
systemctl stop myapp.service || true
systemctl disable myapp.service || true
EOF
chmod 755 myapp/DEBIAN/prerm
 
# DEBIAN/postrm — 卸载后执行
cat > myapp/DEBIAN/postrm << 'EOF'
#!/bin/bash
if [ "$1" = "purge" ]; then
 rm -rf /etc/myapp
 userdel -r myapp 2>/dev/null || true
fi
EOF
chmod 755 myapp/DEBIAN/postrm

构建与安装

# 构建 .deb
dpkg-deb --root-owner-group --build myapp
# 生成 myapp.deb
 
# 检查包内容
dpkg-deb --info myapp.deb
dpkg-deb --contents myapp.deb
 
# 安装
sudo dpkg -i myapp.deb
# 或(自动安装依赖)
sudo apt install ./myapp.deb
 
# 查看已安装的包
dpkg -l | grep myapp
apt list --installed | grep myapp
 
# 卸载
sudo apt remove myapp # 保留配置
sudo apt purge myapp # 删除配置
 
# 查看包中安装了哪些文件
dpkg -L myapp

deb 签名

# 生成 GPG 密钥(如没有)
gpg --full-generate-key
 
# 签名包
dpkg-sig --sign builder myapp.deb
 
# 验证签名
dpkg-sig --verify myapp.deb

62.3 RHEL/Fedora:创建 .rpm 包

设置构建环境

# 安装构建工具
sudo dnf install rpm-build rpmdevtools -y
 
# 设置 RPM 构建目录树
rpmdev-setuptree
# 创建 ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}

.spec 文件

vim ~/rpmbuild/SPECS/myapp.spec
Name: myapp
Version: 1.0.0
Release: 1%{?dist}
Summary: MyApp internal application
 
License: MIT
URL: https://example.com
Source0: %{name}-%{version}.tar.gz
 
Requires: bash, systemd
BuildArch: x86_64
 
%description
MyApp is an internal server application. It provides
a simple service managed by systemd.
 
%prep
%setup -q
 
%install
# 安装到构建根目录
mkdir -p %{buildroot}/usr/local/bin
mkdir -p %{buildroot}/usr/lib/systemd/system
mkdir -p %{buildroot}/etc/%{name}
 
install -m 755 myapp %{buildroot}/usr/local/bin/myapp
install -m 644 myapp.service %{buildroot}/usr/lib/systemd/system/myapp.service
install -m 644 myapp.conf %{buildroot}/etc/%{name}/myapp.conf
 
%pre
# 安装前:创建用户
getent group myapp >/dev/null || groupadd -r myapp
getent passwd myapp >/dev/null || useradd -r -g myapp -s /usr/sbin/nologin myapp
exit 0
 
%post
# 安装后:启用服务
systemctl daemon-reload
systemctl enable myapp.service >/dev/null 2>&1 || :
systemctl start myapp.service >/dev/null 2>&1 || :
 
%preun
# 卸载前:停止服务
if [ $1 -eq 0 ]; then
 systemctl stop myapp.service >/dev/null 2>&1 || :
 systemctl disable myapp.service >/dev/null 2>&1 || :
fi
 
%postun
# 卸载后
if [ $1 -ge 1 ]; then
 systemctl daemon-reload
fi
systemctl daemon-reload
 
%files
%attr(755,root,root) /usr/local/bin/myapp
%attr(644,root,root) /usr/lib/systemd/system/myapp.service
%config(noreplace) /etc/%{name}/myapp.conf
 
%changelog
* Thu Jul 24 2026 Ops Team <ops@example.com> - 1.0.0-1
- Initial release of MyApp

准备源码并构建

# 创建源码 tar.gz(.spec 中 Source0 的文件名)
mkdir ~/rpmbuild/SOURCES/myapp-1.0.0
cp /path/to/myapp-script ~/rpmbuild/SOURCES/myapp-1.0.0/myapp
cp /path/to/myapp.service ~/rpmbuild/SOURCES/myapp-1.0.0/myapp.service
cp /path/to/myapp.conf ~/rpmbuild/SOURCES/myapp-1.0.0/myapp.conf
cd ~/rpmbuild/SOURCES
tar czf myapp-1.0.0.tar.gz myapp-1.0.0/
 
# 构建 RPM
rpmbuild -ba ~/rpmbuild/SPECS/myapp.spec
# -ba:构建二进制包 + 源码包
# -bb:仅构建二进制包
 
# 查看结果
ls ~/rpmbuild/RPMS/x86_64/
# myapp-1.0.0-1.el9.x86_64.rpm

安装与签名

# 安装
sudo dnf install ~/rpmbuild/RPMS/x86_64/myapp-1.0.0-1.el9.x86_64.rpm
 
# 查看包信息
rpm -qip ~/rpmbuild/RPMS/x86_64/myapp-*.rpm
rpm -qlp ~/rpmbuild/RPMS/x86_64/myapp-*.rpm
 
# 查看已安装包
rpm -q myapp
rpm -qi myapp
 
# 卸载
sudo rpm -e myapp
 
# 签名(需配置 GPG)
rpm --addsign ~/rpmbuild/RPMS/x86_64/myapp-*.rpm

常用 macros

# %{_bindir} → /usr/bin
# %{_sbindir} → /usr/sbin
# %{_sysconfdir} → /etc
# %{_unitdir} → /usr/lib/systemd/system
# %{_localstatedir} → /var
# %{_sharedstatedir} → /var/lib
# %{_datadir} → /usr/share
 
# 使用示例
%files
%{_bindir}/myapp
%{_unitdir}/myapp.service
%config(noreplace) %{_sysconfdir}/myapp/myapp.conf

62.4 Arch:PKGBUILD

Arch 打包详见 03-AUR打包与上传,此处给出简要示例:

# PKGBUILD
pkgname=myapp
pkgver=1.0.0
pkgrel=1
pkgdesc="MyApp internal application"
arch=('x86_64')
url="https://example.com"
license=('MIT')
depends=('bash' 'systemd')
source=("$pkgname-$pkgver.tar.gz::https://example.com/myapp-$pkgver.tar.gz")
sha256sums=('SKIP')
 
package() {
 cd "$srcdir/$pkgname-$pkgver"
 install -Dm755 myapp "$pkgdir/usr/local/bin/myapp"
 install -Dm644 myapp.service "$pkgdir/usr/lib/systemd/system/myapp.service"
 install -Dm644 myapp.conf "$pkgdir/etc/myapp/myapp.conf"
}
# 构建
makepkg -si # -s 安装依赖, -i 安装包
# 生成 myapp-1.0.0-1-x86_64.pkg.tar.zst

62.5 搭建私有 apt 仓库(Debian/Ubuntu)

reprepro 方式

sudo apt install reprepro -y
 
# 创建仓库目录
sudo mkdir -p /srv/repo/ubuntu/{conf,dists,incoming,tmp}
sudo chown -R $USER:$USER /srv/repo
 
# 创建仓库配置
vim /srv/repo/ubuntu/conf/distributions
Origin: Example Internal
Label: MyApp Repository
Suite: stable
Codename: jammy
Architectures: amd64 source
Components: main
Description: Internal Ubuntu repository for MyApp
SignWith: yes # 或指定 GPG key ID
# 添加 .deb 包到仓库
reprepro -Vb /srv/repo/ubuntu includedeb jammy myapp_1.0.0-1_amd64.deb
 
# 查看仓库中的包
reprepro -b /srv/repo/ubuntu list jammy
 
# 移除包
# reprepro -b /srv/repo/ubuntu remove jammy myapp

使用 nginx 提供 HTTP 访问

sudo vim /etc/nginx/sites-available/repo
server {
 listen 80;
 server_name repo.example.com;
 
 root /srv/repo/ubuntu;
 autoindex on;
 
 location / {
 try_files $uri $uri/ =404;
 }
}
sudo ln -s /etc/nginx/sites-available/repo /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

客户端配置

# 添加仓库公钥
curl -fsSL https://repo.example.com/repo-key.asc | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/myapp.gpg
 
# 添加 apt 源
echo "deb [arch=amd64] https://repo.example.com jammy main" | sudo tee /etc/apt/sources.list.d/myapp.list
 
# 更新并安装
sudo apt update
sudo apt install myapp

简易 HTTP 方式(快速测试)

# 将 .deb 文件放入某目录
mkdir ~/repo-pool
cp myapp.deb ~/repo-pool/
 
# 生成 Packages 索引
cd ~/repo-pool
dpkg-scanpackages . /dev/null | gzip -9c > Packages.gz
 
# 启动 HTTP 服务
python3 -m http.server 8080
 
# 客户端添加源
echo "deb [trusted=yes] http://server-ip:8080 ./" | sudo tee /etc/apt/sources.list.d/local.list
sudo apt update

62.6 搭建私有 yum/dnf 仓库(RHEL/Fedora)

createrepo 方式

sudo dnf install createrepo -y
 
# 创建仓库目录并放入 RPM
mkdir -p /srv/repo/el9
cp ~/rpmbuild/RPMS/x86_64/myapp-*.rpm /srv/repo/el9/
 
# 生成仓库元数据
createrepo /srv/repo/el9/
 
# 每次添加新包后重新生成
createrepo --update /srv/repo/el9/

Nginx 托管

sudo vim /etc/nginx/conf.d/yum-repo.conf
server {
 listen 80;
 server_name yum-repo.example.com;
 
 root /srv/repo;
 autoindex on;
 
 location ~ /(el[89])/ {
 autoindex on;
 }
}
sudo nginx -t && sudo systemctl reload nginx

客户端配置

# 创建 repo 文件
sudo tee /etc/yum.repos.d/myapp.repo << 'EOF'
[myapp]
name=MyApp Internal Repository
baseurl=https://yum-repo.example.com/el9/
enabled=1
gpgcheck=1
gpgkey=https://yum-repo.example.com/repo-key.asc
EOF
 
# 更新并安装
sudo dnf makecache
sudo dnf install myapp
 
# 查看可用包
dnf --repo=myapp list available

62.7 Ostree 与 rpm-ostree(不可变系统分发)

对于 Fedora CoreOS、Fedora Silverblue 等不可变系统,使用 rpm-ostree 创建自定义系统镜像:

# 安装 ostree 和 rpm-ostree
sudo dnf install rpm-ostree ostree -y
 
# 查看当前部署
rpm-ostree status
 
# 在不可变系统中层叠安装包
rpm-ostree install vim htop git
 
# 更新到新的 commit
rpm-ostree update
 
# 查看包的 commit 来源
rpm-ostree db list | head -20

创建自定义 OSTree 仓库进行系统级分发:

# 创建 OSTree 仓库
mkdir -p /srv/ostree
ostree --repo=/srv/ostree init --mode=archive
 
# 从现有 compose 导入
rpm-ostree compose tree --repo=/srv/ostree fedora-silverblue.yaml
 
# 通过 HTTP 提供
# nginx 配置:root /srv/ostree; autoindex on;

详见 48-不可变系统


62.8 Flatpak:桌面应用分发

Flatpak 是跨发行版的桌面应用分发方案:

# 安装 Flatpak SDK
sudo apt install flatpak flatpak-builder -y # Debian
sudo dnf install flatpak flatpak-builder -y # RHEL
 
# 创建 Flatpak manifest
vim com.example.myapp.yml
app-id: com.example.myapp
runtime: org.gnome.Platform
runtime-version: '46'
sdk: org.gnome.Sdk
command: myapp
 
modules:
 - name: myapp
 buildsystem: simple
 build-commands:
 - install -Dm755 myapp /app/bin/myapp
 sources:
 - type: file
 path: myapp
 
finish-args:
 - --socket=wayland
 - --socket=fallback-x11
 - --share=network
# 构建
flatpak-builder build-dir com.example.myapp.yml --force-clean
 
# 导出到本地仓库
flatpak-builder --repo=myrepo --force-clean build-dir com.example.myapp.yml
 
# 安装
flatpak --user install myrepo com.example.myapp
 
# 运行
flatpak run com.example.myapp

62.9 容器镜像作为分发方式

对于服务端应用,OCI 容器镜像是最通用的分发格式:

# Dockerfile
FROM debian:bookworm-slim
 
RUN apt update && apt install -y --no-install-recommends \
 ca-certificates curl && \
 rm -rf /var/lib/apt/lists/*
 
COPY myapp /usr/local/bin/myapp
RUN chmod +x /usr/local/bin/myapp
 
RUN useradd -r -s /usr/sbin/nologin myapp
USER myapp
EXPOSE 8080
HEALTHCHECK --interval=30s CMD curl -f http://localhost:8080/health || exit 1
 
ENTRYPOINT ["/usr/local/bin/myapp"]
# 构建
docker build -t registry.example.com/myapp:1.0.0 .
docker push registry.example.com/myapp:1.0.0
 
# 目标服务器拉取运行
docker run -d --name myapp -p 8080:8080 registry.example.com/myapp:1.0.0

详见 46-容器技术


62.10 自动化打包流水线

GitHub Actions 自动构建 .deb

# .github/workflows/build-deb.yml
name: Build DEB Package
 
on:
 push:
 tags: ['v*']
 
jobs:
 build:
 runs-on: ubuntu-latest
 steps:
 - uses: actions/checkout@v4
 
 - name: Build DEB
 run: |
 mkdir -p myapp_${GITHUB_REF_NAME#v}/DEBIAN
 mkdir -p myapp_${GITHUB_REF_NAME#v}/usr/local/bin
 cp myapp myapp_${GITHUB_REF_NAME#v}/usr/local/bin/
 # ... 复制 control, service 等
 dpkg-deb --root-owner-group --build myapp_${GITHUB_REF_NAME#v}
 
 - name: Upload artifact
 uses: actions/upload-artifact@v4
 with:
 name: myapp-deb
 path: myapp_*.deb
 
 - name: Push to repo
 run: |
 curl -X POST -F "file=@myapp_*.deb" \
 https://repo.example.com/upload?token=${{ secrets.REPO_TOKEN }}

简单的 Watch + Build 脚本

#!/bin/bash
# watch-build.sh — 监控源码变化,自动重新打包
set -euo pipefail
 
REPO_DIR="/srv/repo/ubuntu"
APP_DIR="/opt/myapp"
 
while true; do
 inotifywait -e modify,create,delete -r "$APP_DIR"
 
 VERSION=$(date +%Y%m%d%H%M%S)
 echo "检测到变更,构建版本 $VERSION..."
 
 # 包装你的构建逻辑
 mkdir -p /tmp/myapp-pkg/DEBIAN
 mkdir -p /tmp/myapp-pkg/usr/local/bin
 cp "$APP_DIR/myapp" /tmp/myapp-pkg/usr/local/bin/
 
 cat > /tmp/myapp-pkg/DEBIAN/control << EOF
Package: myapp
Version: 0.0.${VERSION}
Architecture: amd64
Maintainer: CI <ci@example.com>
Description: MyApp auto-built package
EOF
 
 dpkg-deb --root-owner-group --build /tmp/myapp-pkg
 reprepro -Vb "$REPO_DIR" includedeb stable /tmp/myapp-pkg.deb
 rm -rf /tmp/myapp-pkg*
 
 echo "构建并推送到仓库完成"
done

62.11 常见问题

问题原因解决
deb 安装依赖错误control 文件中依赖写错apt install ./xxx.deb 自动处理
rpm 构建签名失败无 GPG 密钥gpg --full-generate-key 创建
reprepro “not found”架构或 codename 不匹配检查 conf/distributions 配置
createrepo 404路径错误/Nginx root 不对确认 root 和 alias 匹配
包安装后服务未启动缺少 postinst/post 脚本添加 systemctl enable/start
dpkg-deb 权限问题文件列表不是 root 所有--root-owner-group 参数

62.12 本章总结

打包方式适用发行版核心命令复杂度
dpkg-debDebian/Ubuntudpkg-deb --build
rpmbuildRHEL/Fedorarpmbuild -ba
PKGBUILD + makepkgArchmakepkg -si
repreproDebian/Ubuntu 仓库reprepro includedeb
createrepoRHEL/Fedora 仓库createrepo --update
Flatpak跨发行版桌面应用flatpak-builder中高
OCI 容器镜像所有 Linuxdocker build && push
Ostree/rpm-ostreeFedora 不可变系统rpm-ostree compose

包管理基础见 14-软件包管理通识,Arch 的 AUR 打包详见 03-AUR打包与上传,不可变系统详见 48-不可变系统