自建内网 APT 仓库实战:用 reprepro 统一分发 deb 包,版本可控、离线可用、批量部署不再逐台敲命令

为什么要自建内网 APT 仓库

如果你手上有三台以上的服务器,就一定经历过这种场面:一批机器要装同一个软件,于是你在每台机器上敲一遍 apt update && apt install -y 某个包。表面上看没问题,直到有一天官方源抖动、或者某个包被上游下架、或者你要装的是一份自己编译的 deb——这时候你才发现,把"装软件"这件事完全托付给公网第三方,是一件多么不踏实的事。

自建 APT 仓库解决的正是这类问题。它把 deb 包集中托管在一台你完全掌控的机器上,其他服务器把它当成一个普通的 apt 源来用。带来的直接收益有四条:

  • 版本可控:你决定服务器上跑哪个版本的软件,而不是被上游"最新即最好"推着走。生产环境锁版本,是减少"升级后突然跑不起来"的第一道防线。
  • 离线可用:内网机器、被墙的机器、安全组只放行内网的机器,照样能装包。
  • 速度快:内网千兆/万兆拉包,比公网快一两个数量级,批量部署时体验差别巨大。
  • 合规与审计:每个 deb 都经过你的服务器,装了什么一目了然,也方便做签名校验与留存。

本文用 reprepro 来搭,原因是它足够轻、足够透明——一个本地目录 + 一个命令,所有元数据都是明文文件,出了问题用 ls 就能看懂。相比 aptly 那种"藏一个数据库在后面"的方案,reprepro 更适合个人站长和小团队。

先搞清 APT 仓库到底由什么构成

很多人一上来就跑命令,结果配完了 apt update 报一堆看不懂的错。原因是没有先建立"仓库由哪些文件组成"的心智模型。一个最小的、能被 apt 认出来的仓库,包含三层东西:

  1. 包的实体:也就是 .deb 文件本身,放在某个目录下(通常按 pool/ 组织)。
  2. 索引(metadata):apt 真正读的不是 deb,而是索引文件。核心是 Packages.gz(未压缩叫 Packages),它列出了每个包的名字、版本、依赖、文件名、校验和。apt 靠它决定"有哪些包可装、装哪个版本、要去哪个路径下载"。
  3. Release 与签名:Release 文件描述了某个发行版/组件/架构组合下所有索引的校验和。它旁边的 Release.gpg(或 InRelease)是它的 GPG 签名。apt 之所以信任一个源,就是因为能拿本地信任的 GPG 公钥验证这个签名。

这三层对应的正是 apt 源配置文件里那行的结构:

deb [signed-by=/etc/apt/keyrings/myrepo.gpg] http://192.168.1.10/debian bookworm main

拆开看:deb 是类型,[signed-by=...] 指定用哪个公钥验签,URL 是仓库根路径,bookworm 是发行版代号(distribution),main 是组件(component)。apt 会去请求 URL/dists/bookworm/Release,再根据 Release 里记录的路径去找 URL/dists/bookworm/main/binary-amd64/Packages.gz。任何一个环节对不上(目录名拼错、组件名不一致、签名验不过),都会在 apt update 阶段就直接失败。

理解这个结构后,reprepro 做的事情就很好理解了:你把 deb 丢给它,它负责按这套目录规范和索引格式自动生成文件,让你不用手工去 gzip 和写校验和。

安装 reprepro 并初始化一个仓库

在作为仓库服务器的机器上(Debian/Ubuntu 均可):

apt install -y reprepro gnupg

准备一个目录作为仓库根,比如 /srv/aptrepo,并给它建好对应的发行版目录结构。reprepro 需要一个配置文件 conf/distributions 来描述"有哪些发行版、每个发行版的组件和架构"。先建目录:

mkdir -p /srv/aptrepo/conf
mkdir -p /srv/aptrepo/db

然后写 /srv/aptrepo/conf/distributions:

Origin: MyCompany
Label: MyCompany Internal
Codename: bookworm
Architectures: amd64
Components: main
Description: Internal APT repository for Debian bookworm
SignWith: internal-repo-key

这里每个字段都有实际含义,不要乱写:

  • Codename 必须和你客户端 sources.list 里的发行版代号完全一致(bookworm 不是 Bookworm)。这是最常出错的地方之一。
  • Components 列出的组件名,必须和客户端行里的组件名一致。
  • Architectures 写你要托管的架构。只写 amd64 就不会生成 arm64 的索引,客户端如果是别的架构会提示"找不到包"。
  • SignWith 指定签名用的 GPG 密钥 ID 或名称,下面会生成。

有多个发行版(比如同时服务 bookworm 和 bullseye),就在这个文件里写多段,每段用空行隔开。

生成 GPG 密钥并让仓库签名

apt 默认要求仓库签名,否则会报 NO_PUBKEY 或者直接拒绝。所以先给仓库造一把专用的 GPG 密钥,不要用别人的、也不要用服务器的 root 主密钥——一把专钥专用,泄露了也好撤换。

gpg --batch --gen-key <<EOF
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: internal-repo-key
Name-Email: repo@example.internal
Expire-Date: 0
%commit
EOF

%no-protection 表示私钥不设密码,这样 reprepro 在 cron 里自动签名时不会卡在交互输入上。代价是私钥以明文存在 ~/.gnupg,所以这台机器本身的安全等级要提高:限制登录、把仓库写权限只给专门的用户。如果在意这一点,可以给私钥设密码,再用 gpg-agent 缓存;但对个人站长来说,%no-protection + 机器加固是更实际的取舍。

生成后确认密钥存在,并记住它的指纹:

gpg --list-secret-keys --keyid-format LONG

输出里那串 40 位十六进制就是指纹。把 conf/distributions 里的 SignWith 改成这把密钥的 ID 或 Name-Real(internal-repo-key),reprepro 就能找到它。为了以后换机器方便,建议导出私钥备份:

gpg --export-secret-keys internal-repo-key > /root/repo-key.sec
gpg --export --armor internal-repo-key > /srv/aptrepo/repo-key.pub

后者(公钥)后面要拷到每台客户端,前者(私钥)离线保存,别和仓库一起放在被备份的目录里。

把 deb 包加进仓库

reprepro 添加包的基本命令非常直白:

cd /srv/aptrepo
reprepro --basedir /srv/aptrepo includedeb bookworm /path/to/foo_1.2.3_amd64.deb

它会自动把 deb 复制到 pool/ 下合适的位置,更新 Packages 索引,重新生成 Release 并签名。成功后你 ls 一下 /srv/aptrepo/dists/bookworm/,应该能看到 Release、InRelease、main/binary-amd64/Packages.gz 这一整套文件。

几个实用变体:

# 移除某个包
reprepro --basedir /srv/aptrepo remove bookworm foo

# 一次加多个(用通配)
reprepro --basedir /srv/aptrepo includedeb bookworm /tmp/debs/*.deb

# 查看仓库里有哪些包
reprepro --basedir /srv/aptrepo list bookworm

如果你想托管的 deb 是从别的机器上 apt download 下来的,那些包往往带着版本号后缀。reprepro 会根据 deb 内部的 control 信息解析出包名与版本,不用你改名。

用 Nginx 把仓库发出去

仓库是纯静态文件,用 Nginx 托管最合适。一个最小配置:

server {
    listen 80;
    server_name repo.example.internal;

    root /srv/aptrepo;
    autoindex on;

    # apt 会请求 gz 索引,确保不被错误地当成文本改写
    types {
        application/x-gzip gz;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

autoindex on 只是为了浏览器里能直接看目录,apt 实际不用它。真正要注意的是不要让 Nginx 去修改这些文件的响应体(比如某些压缩或改写模块),否则校验和会对不上,apt 会报 Hash Sum mismatch。如果前面套了 CDN,也务必确认索引文件没有被缓存到过期——镜像源索引更新后客户端拿到的必须是新的 Release,否则签名与索引不匹配。

客户端怎么接进来

在每台客户端上,先把仓库公钥放进"可信密钥"目录。推荐用独立的 keyrings 目录,而不是老式的 apt-key add(apt-key 已被 Debian 标记为废弃,会污染全局信任):

mkdir -p /etc/apt/keyrings
gpg --dearmor < repo-key.pub > /etc/apt/keyrings/myrepo.gpg
chmod 644 /etc/apt/keyrings/myrepo.gpg

然后写源文件 /etc/apt/sources.list.d/myrepo.list:

deb [signed-by=/etc/apt/keyrings/myrepo.gpg] http://repo.example.internal/ bookworm main

注意 URL 结尾最好带斜杠,组件名和发行版代号要和仓库配置严格对应。接着:

apt update
apt-cache policy 你的包名

apt-cache policy 的输出会显示每个可用版本来自哪个源。如果只看到官方源、没有你自建的源,说明源文件没被读到或发行版/组件名对不上。如果看到源但报签名错误,就去查 keyring 路径和公钥是否正确。

几个最容易踩的坑

自建仓库的坑,九成出在"名字对不上"和"签名"这两件事上,逐一列清楚:

  • 发行版代号大小写/拼写不符:客户端写 Bookworm、仓库配置写 bookworm,apt 会去请求一个不存在的 dists/Bookworm/,报 404。这个错和"仓库里没这个包"表现不同,注意区分。
  • 组件名不一致:配置里是 main,客户端写 stable,同样 404。
  • 架构不匹配:你只导入 amd64 包并只声明了 amd64 架构,客户端是 arm64 就会说找不到。
  • 公钥没换行/没 dearmor:直接把 ASCII-armored(-----BEGIN PGP----- 那串)公钥当二进制用,或反之,apt 直接报无法解析。记住客户端要的是 gpg --dearmor 后的二进制格式。
  • 私钥密码导致签名失败:cron 里跑 reprepro 时若私钥有密码且没有 agent,签名会静默失败或卡住,仓库索引停留在旧版本。用 %no-protection 或配好 gpg-agent 可避免。
  • Hash Sum mismatch:多半是中间有代理/CDN 缓存了旧索引,或者拷贝仓库时漏了文件。清掉客户端 /var/lib/apt/lists/ 里的缓存重试,并检查仓库文件是否完整。
  • Nginx 目录权限:Nginx 的 worker 用户(通常是 www-data)必须有读权限进到 /srv/aptrepo 每一级目录。用 namei -l /srv/aptrepo/dists/bookworm/Release 逐级查 x 位,比猜权限快得多。

自动化与日常维护

仓库搭好后,把它纳入日常运维的关键是"导入由脚本做、签名由 cron 保证"。一个简单的导入脚本骨架:

#!/bin/bash
set -euo pipefail
REPO=/srv/aptrepo
DIST=bookworm
STAGING=/var/tmp/debs
# 校验每个 deb 都是合法的 deb(避免半截下载的文件进仓库)
for d in "$STAGING"/*.deb; do
    dpkg-deb --info "$d" >/dev/null
done
reprepro --basedir "$REPO" includedeb "$DIST" "$STAGING"/*.deb
echo "已导入 $(ls "$STAGING"/*.deb | wc -l) 个包"

dpkg-deb --info 会在文件损坏时非零退出,配合 set -e,能让一个坏文件挡住整批导入而不是混进仓库。这是很值得加的一步——一个损坏的 deb 进了仓库,所有客户端 pull 时都会受影响。

日常检查清单可以固定成三条:

  1. reprepro list bookworm 确认包清单符合预期。
  2. curl -sI http://repo.../dists/bookworm/Release 或检查 Release 里的 Date 字段,确认索引是新的。
  3. 在一台闲置客户端上跑 apt update && apt install --dry-run 你的包 做一次冒烟,确保端到端可用。

什么时候该用它、什么时候别用

自建 APT 仓库不是所有场景都值得。它适合:需要锁定版本、需要离线/内网分发、需要在多台机器上批量部署同一批自定义 deb 的场合。它不适合:单台机器、只装一两个包、且公网源稳定的场景——这时候你只是在给自己增加一个需要维护的组件,反而多了一个可能出错的地方。

判断标准很简单:如果你每次都要重复"在多台机器上装同样的包"这个动作,且对版本或网络有要求,那就值得建;如果只是偶尔装一下,直接 apt install 更省事。技术选型的第一条永远是"解决真实痛点",而不是"看起来很专业"。

Last modification:October 7th, 2026 at 08:24 pm

Leave a Comment