搭建 APT 镜像

这里以 debian 为例子, 一般能够看到为了国内源从而配置修改相关的 sources.list 等文件, 目前有以下集中源配置:

  • sources.list: 直接源配置
  • DEB822: 新的安全配置源

比如下面的的源格式:

1
2
3
4
5
6
7
8
9
# sources.list 经典的远程源配置
deb http://mirrors.ustc.edu.cn/debian trixie main contrib non-free non-free-firmware

# DEB822 格式远程源配置, 追加 gpg 签名做安全验证, 'SignWith: no' 代表不启用签名验证
Types: deb
URIs: http://mirrors.ustc.edu.cn/debian
Suites: trixie trixie-updates
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg

如果要自己做内网二进制打包镜像, 可以采用 sources.list 配置, 因为本身内网的包是管理员确定已提交的可控安装包.

注意: 提交暴露给内网的包在 debian 系都是 的 *.deb 结尾打出的压缩包

我们先通过 sources.list 格式来解读我们的镜像源需要什么配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 以 'deb http://mirrors.ustc.edu.cn/debian trixie main contrib non-free non-free-firmware' 为例子
# deb|deb-src: deb 表示二进制包仓库, 也就是直接拉取 deb 包安装; deb-src 代表源码包拉取, 不会做编译处理
# http://mirrors.ustc.edu.cn/debian: 代表远程镜像地址
# trixie: 代表目前系统系统代号, 可以通过 cat /etc/os-release|grep VERSION= 查看
# main contrib non-free non-free-firmware: 这里代表的索引目录, 也就远程带有以下目录(以 /pool 为根目录)
# - http://远程镜像/pool/main: 代表 debian 负责官方维护的包, 比如编译工具和开发工具链等
# - http://远程镜像/pool/contrib: 代表 debian 半官方支持, 比如依赖的第三方含有非开源的非自由工具包
# - http://远程镜像/pool/non-free: 代表闭源的非自由软件包, 具有闭源/禁止修改/分发/商业授权等限制
# - http://远程镜像/pool/non-free-firmware: 硬件设备的闭源固件(Firmware), 定制驱动的非自由组件包
# ===========================================================================================
# 按照样例来看, 最后需要搭建的远程目录结构如下
#
http://mirrors.ustc.edu.cn/debian/
├─ dists/ # 包索引目录(APT 更新时下载这里的索引)
│ └─ trixie/ # 对应发行版代号(trixie=Debian 13)
│ ├─ main/ # main 组件的索引
│ │ ├─ binary-amd64/ # amd64 架构的索引文件(Packages、Packages.gz)
│ │ ├─ source/ # 源码包索引(Sources)
│ │ └─ ...
│ ├─ contrib/ # contrib 组件的索引(结构同 main)
│ ├─ non-free/ # non-free 组件的索引(结构同 main)
│ └─ non-free-firmware/ # non-free-firmware 组件的索引(结构同 main)
└─ pool/ # 实际 .deb 包存储目录(对应组件分类)
├─ main/ # main 组件的包
│ ├─ a/ # 按软件名首字母划分(如 apache2)
│ ├─ b/ # 如 bash、nginx(n 开头)
│ └─ ...
├─ contrib/ # contrib 组件的包(结构同 main)
├─ non-free/ # non-free 组件的包(结构同 main)
└─ non-free-firmware/ # non-free-firmware 组件的包(结构同 main)

只需要按照这部分目录构建就可以自定义生成主要的架构, 如果想要安装过程自动进行打包就需要手动来构建 {包}.deb 包,
这里按照流程生成所需要镜像源的本地目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 一般采用 nginx + reprepro 做轻量级的包编译即可
# reprepro 是 Debian 系专用的 APT 仓库管理工具, 能自动生成索引, 管理包的增删和处理发行版/组件映射
sudo apt install nginx nginx-extras reprepro dpkg-dev

# 设定 /data/mirror/debian 为本地包的访问路径
# 这里就需要创建 conf,dists,pool 这三个目录
sudo mkdir -p /data/mirror/debian/{conf,dists,pool}

# 按组件和发行版划分目录(对应 trixie + main/contrib 等组件)
# 注意这里声明这部分二进制包是采用 amd64 架构, 如果采用 arm 就需要额外处理
sudo mkdir -p /data/mirror/debian/dists/trixie/{main,contrib,non-free,non-free-firmware}/binary-amd64
sudo mkdir -p /data/mirror/debian/pool/{main,contrib,non-free,non-free-firmware}

# 需要创建 distributions 文件用于描述目前的仓库信息
sudo touch /data/mirror/debian/conf/distributions

# 赋予 nginx 访问权限
sudo chown -R www-data:www-data /data/mirror
sudo chmod -R 755 /data/mirror

具体创建目录说明:

  • conf: 存放 reprepro 的配置文件(核心目录)
  • dists: 存放 APT 索引文件(Packages/Release 等, 由 reprepro 自动生成)
  • pool: 存放实际的 .deb 包(按组件分类)

这里需要声明该镜像库目前的信息(/data/mirror/debian/conf/distributions):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 以 www-data 权限生成源仓库信息
# 注意: 我这里因为采用 Ubuntu 24.04.3 LTS (Noble Numbat)
# Ubuntu24.04 是 debian13 的下游, 而 noble 的软件库可以和 trixie 的软件复用
# 因为我演示是在 Ubuntu 设备上处理, 而我日常服务器都是 debian 系统, 所以为了方便这里声明为 ubuntu
sudo -u www-data bash -c 'cat > /data/mirror/debian/conf/distributions << EOF
# 发行版代号(需与系统一致, 如 debian13 - trixie | debian12 - bookworm | ubuntu22.04 - jammy | ubuntu24.04 - noble)
Codename: noble
# 仓库描述
Description: MeteorCat APT Repository
# 支持的组件(与 pool 目录对应)
Components: main contrib non-free non-free-firmware
# 支持的架构(amd64 为主, 可加 arm64 armhf i386 等架构, 但是必须确保架构的包存在)
Architectures: amd64
# 索引文件压缩格式
UDebComponents: main
EOF'

# 生成完成之后就是通过 nginx 配置加载
sudo bash -c 'cat > /etc/nginx/conf.d/apt-debian.conf << EOF
server {
listen 80; # 端口按需求修改
# 可选:填写你的服务器域名(如apt.meteorcat.local),内网用IP可留空为_
server_name _;

# 指向你的仓库根目录
root /data/mirror;

## 开启目录列表
# autoindex on;
#autoindex_exact_size off;
# autoindex_localtime on;

# 不过这里做了美化采用 fancyindex
location ^~ / {
fancyindex on; # 启用fancyindex
fancyindex_exact_size off; # 人性化文件大小
fancyindex_localtime on; # 本地时间
fancyindex_name_length 255; # 显示完整文件名
# 忽略 .log 结尾|.tmp 结尾 | 隐藏文件(以.开头)| test 目录
fancyindex_ignore "\.log$|\.tmp$|^\..+$|^test$";
}

# 日志配置(便于排查访问问题)
access_log /var/log/nginx/apt-debian-access.log;
error_log /var/log/nginx/apt-debian-error.log;


# 规则1:禁止访问所有.log/.tmp后缀的文件(任意目录下)
location ~* \.(log|tmp)$ {
deny all;
return 404; # 伪装成资源不存在,比403更安全
}

# 规则2:禁止访问根目录下的.test/.*隐藏目录/文件(任意目录下)
location ~* /(\..+|test) {
deny all;
return 404;
}
}
EOF'

启动 nginx 就可以访问到镜像地址目标即可, 之后就是添加安装包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 重启 Nginx
sudo systemctl restart nginx


# 这里以官方的 htop 工具为例子, 确认先卸载掉本地的包并清理缓存
sudo apt purge htop
sudo apt autoremove
sudo apt autoclean

# 我们假借中科大的包下载过来当作我们自己镜像包内部的 htop 软件
cd /data/mirror/debian/ # 必须进入源镜像目录

# 我们先把远程下载到包放在临时目录
# 注意临时目录的文件需要删除下避免空间不足
sudo -u www-data wget -O /tmp/htop_3.4.1-5_amd64.deb https://mirrors.ustc.edu.cn/debian/pool/main/h/htop/htop_3.4.1-5_amd64.deb

# 执行 reprepro 索引, 这里的配置是有规则的
# reprepro includedeb: 代表引入包
# noble: 这里是 distributions 文件声明的 Codename 配置
sudo -u www-data reprepro includedeb noble /tmp/htop_3.4.1-5_amd64.deb

# 如果想删除包和其索引, 可以通过以下指令
# 注意: 也是必须要进入到源镜像目录
sudo -u www-data reprepro list noble # 查看导入的的包列表
sudo -u www-data reprepro remove noble htop # 注意: 删除包只需要关键字, 比如 htop

之后其他服务器配置镜像源直接声明下 nginx 地址配置即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 这里直接映射部署的 nginx 地址
sudo echo "deb http://127.0.0.1/debian noble main contrib non-free non-free-firmware" | sudo tee /etc/apt/sources.list.d/apt-debian.list

# 更新源
sudo apt update
# 这里会报错: 仓库 “http://127.0.0.1/debian noble Release” 没有数字签名
# 如果是对外开放的服务一定要做好签名验证
# 而如果是本地认证绝对不会出问题的包, 可以跳过这部分认证, 这需要将镜像源配置修改如下
sudo echo "deb [trusted=yes] http://127.0.0.1/debian noble main contrib non-free non-free-firmware" | sudo tee /etc/apt/sources.list.d/apt-debian.list
# 不过还是建议对包做签名

# 最后就是安装 htop, 如果本地已经安装可以执行 sudo apt upgrade 自动升级
sudo apt install htop

后续就是其他细节, 比如生成 GPG 签名(强烈建议)和数据缓存和自动更新镜像源,
不过内网使用一般搞个 https 证书安装到服务器设置 [trusted=yes] 基本上可以满足这部分日常需求.

deb 打包

这里提供源码打包样例, 采用将 jenkins 打包安装到系统安装, 然后放置在自己镜像源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 先进拉取官方包
cd /tmp && wget https://get.jenkins.io/war-stable/2.528.1/jenkins.war jenkins.war

# 创建对应包, 标记为 amd64 包
mkdir -p /tmp/jenkins_2.528.1_amd64 && cd /tmp/jenkins_2.528.1_amd64


# 创建 DEB 包信息目录
mkdir -p DEBIAN && cd DEBIAN
touch control # 设置信息文件


# postinst(安装后脚本) | prerm(卸载前脚本)
# 注意权限必须要 <= 775, 否则会出现包安装失败
touch postinst && chmod 755 postinst
touch prerm && chmod 755 prerm

# 编辑 control 文件
sudo vim /tmp/jenkins_2.528.1_amd64/DEBIAN/control

首先编写 control 的软件包信息:

1
2
3
4
5
6
7
8
Package: jenkins
Version: 2.528.1
Description: The leading open source automation server, Jenkins provides hundreds of plugins to support building, deploying and automating any project.
Section: net
Priority: standard
Architecture: amd64
Maintainer: jenkins.io
Pre-Depends: fontconfig, openjdk-21-jre, git, subversion

之后创建映射的安装目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建源码目录
mkdir -p /tmp/jenkins_2.528.1_amd64/usr/local/src/jenkins

# 将之前下载的 war 包放置于 src 目录
cp -rf /tmp/jenkins.war /tmp/jenkins_2.528.1_amd64/usr/local/src/jenkins/jenkins.war
chmod 755 /tmp/jenkins_2.528.1_amd64/usr/local/src/jenkins/jenkins.war

# 源码包直接放在在以下目录
# ls -l /tmp/jenkins_2.528.1_amd64/usr/local/src/jenkins/jenkins.war

# 之后就编写安装|卸载的脚本
vim postinst # 安装脚本
vim prerm # 卸载脚本

# 其实这里应该创建 /tmp/jenkins_2.528.1_amd64/etc/systemd/system/jenkins.service 服务启动相关
# 但是为了节约时间这里不做扩展

postinst 安装脚本本身就是运行脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh
set -e # 出错立即退出

# 这里最好检测下 java -version 确认二进制存在或者版本存在
java -version

# 创建 jenkins 用户和组
if ! id -u jenkins >/dev/null 2>&1; then
useradd -r -m -d /var/lib/jenkins -s /bin/false jenkins
fi

# 授权目录
chown -R jenkins:jenkins /usr/local/src/jenkins

prerm 卸载脚本也是个运行脚本:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh

set -e

# 停止并禁用服务
if systemctl is-active --quiet jenkins.service; then
systemctl stop jenkins.service
fi
systemctl disable jenkins.service

# 卸载时删除用户(可选,保留数据则注释)
userdel -r jenkins

现在已经可以进行打包处理:

1
2
3
4
5
# 回到包的根目录并且启动打包程序
cd /tmp && dpkg -b /tmp/jenkins_2.528.1_amd64 jenkins_2.528.1_amd64.deb

# 打包完成可以看到输出
ls -l /tmp/jenkins_2.528.1_amd64.deb

至此 deb 已经打包完成, 这里先不要放置在镜像源, 测试下安装本地的时候是否会进行编译处理:

1
2
3
4
5
6
7
8
9
10
11
# 测试安装, 会进入编译流程
# 这里就会提示 openjdk-21-jre 未安装, 这就是强依赖的应用
sudo dpkg -i /tmp/jenkins_2.528.1_amd64.deb

# 测试没问题可以卸载安装包
sudo dpkg -r jenkins

# 之后直接提交到自己镜像源网站
cd /data/mirror/debian/ # 进入镜像源目录
sudo -u www-data reprepro includedeb noble /tmp/jenkins_2.528.1_amd64.deb
sudo -u www-data reprepro list noble # 查看是否提交包完成

这里就是怎么手动打出 deb 包并且提交的流程.

安全签名

之前虽然能够采用 [trusted=yes] 跳过安全认证, 但是为了安全最好还是手动生成数字签名来提供服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# 安装 GPG 工具
sudo apt update && sudo apt install -y libconfig-file-perl gnupg dpkg-sig
# 注意: 很奇葩的一点就是 dpkg-sig 这个包可能 Ubuntu 环境下 apt 找不到
# 需要去上游额外的 debian 源下载安装:
wget -O /tmp/dpkg-sig_0.13.1+nmu4_all.deb https://mirrors.ustc.edu.cn/debian/pool/main/d/dpkg-sig/dpkg-sig_0.13.1%2Bnmu4_all.deb
sudo dpkg -i /tmp/dpkg-sig_0.13.1+nmu4_all.deb # 安装 dpkg-sig

# 查看具体版本信息
gpg --version

# 先手动生成 GPG 密钥
# 注意: 生成的密钥信息会默认保存 ~/.gnupg/openpgp-revocs.d 之中
# 所以如果要自动化生成的话, 最好确认授权是否存在 HOME 目录保证生成
# 这里也是异常高发地, 会涉及到挂载的服务用户权限问题, 后面会说明
gpg --full-generate-key
# 请选择您要使用的密钥类型: 选择 {(4) RSA(仅用于签名)|RSA(sign only) } 选项
# 选择 RSA 密钥的长度: 4096
# 你要有效时限: 0 - 永不过期
# 确认信息进行下一步, 后续就用仓库具体信息
# 需要输出真实名称: MeteorCat(我这里随便自定义名称)
# 需要输入邮箱地址: repo@meteorcat.me(输入自己邮箱)
# 注释说明: 这个随便, 留空也行
# 输入 o 生成, 其中会提示你需要密码保护, 如果不想要额外设置密码也没事, 直接继续下一步即可

# 生成之后按照流程一般是导出公钥到镜像源的源目录下暴露给 apt 源配置
# 导出公钥需要采用之前输入邮箱地址指定, 比如上面说的 repo@meteorcat.me
gpg --armor --export repo@meteorcat.me > /tmp/meteorcat-debian-mirror.gpg.key

# 之后把公钥放到镜像源目录下
# 后续如果其他服务需要配置源就需要执行以下命令来导入公钥
# wget -O - http(s)://{源镜像站}/debian/meteorcat-debian-mirror.gpg.key | apt-key add -
sudo -u www-data cp /tmp/meteorcat-debian-mirror.gpg.key /data/mirror/debian/meteorcat-debian-mirror.gpg.key


# -------------------------------------------------------------------------
# 这里就是假设生成的签名数据
# gpg: 吊销证书已被存储为‘ /home/meteorcat/.gnupg/openpgp-revocs.d/906B7B5387FA4D2C445795D737B153163F339E56.rev’
# 公钥和私钥已经生成并被签名。
#
# pub rsa4096 2025-12-01 [SC]
# 906B7B5387FA4D2C445795D737B153163F339E56
# uid MeteorCat <repo@meteorcat.me>
# sub rsa4096 2025-12-01 [E]
# -------------------------------------------------------------------------
# pub 下面的 906B7B5387FA4D2C445795D737B153163F339E56 就是生成的 KEY ID, 也就是我们后续需要的签名标识
# 我们需要的是这个标识的后8位

# 修改 /data/mirror/debian/conf/distributions 文件
# 需要追加配置签名 SignWith 字段
# 同时需要 GPG 生成的 KEY: 906B7B5387FA4D2C445795D737B153163F339E56
# 那么我们需要追加个 'SignWith: yes' 配置
sudo vim /data/mirror/debian/conf/distributions
# 最后生成的仓库元数据内容如下(首行#不需要加上):
# # 发行版代号(需与系统一致, 如 debian13 - trixie | debian12 - bookworm | ubuntu22.04 - jammy | ubuntu24.04 - noble)
# Codename: noble
# # 仓库描述
# Description: MeteorCat APT Repository
# # 支持的组件(与 pool 目录对应)
# Components: main contrib non-free non-free-firmware
# # 支持的架构(amd64 为主, 可加 arm64 armhf i386 等架构, 但是必须确保架构的包存在)
# Architectures: amd64
# # 索引文件压缩格式
# UDebComponents: main
# # GPG 密钥签名
# SignWith: yes

# 准备就是对包进行签名处理
# 这是远程下载的 deb 包 or 自己打包出来的 deb, 以之前说过的下载的上级 deb 包为例子, 我们重新再走一遍流程
wget -O /tmp/htop_3.4.1-5_amd64.deb https://mirrors.ustc.edu.cn/debian/pool/main/h/htop/htop_3.4.1-5_amd64.deb

# 对deb 包签名写入
# 注意: 这里打包的是刚下载下来的 deb 包, 而不是已经放在镜像源的 deb 包
dpkg-sig --sign builder -k 3F339E56 /tmp/htop_3.4.1-5_amd64.deb
# 输出 Signed deb /tmp/htop_3.4.1-5_amd64.deb 就代表签名结束
# 之后就是把签名后的包提交镜像源

# 这里则是利用新的 -b/--basedir 参数指定镜像源目录, 如果你之前对 htop 提交过一次源镜像, 以下命令会执行报错:
sudo -u www-data reprepro -b /data/mirror/debian remove noble htop # 清理原来的包内容
sudo -u www-data reprepro --basedir /data/mirror/debian includedeb noble /tmp/htop_3.4.1-5_amd64.deb
# 需要注意这里会常常出错提示 Already existing files can only be included again
# 这是因为默认的包都会被文件哈希做对比, 你重新引入的 deb 包和之前的老 deb 文件哈希不一致
# 按照源配置的原理: 同样版本的包被发布出去, 就不能再被修改, 只能被版本号更新之后同步!
# 所以要求源配置提交一定要小心! 一旦执行 includedeb 之后同名同版本的包就不被允许覆盖而是必须升级版本来提交!


# 所以我们这里就需要重新在上游找个源包签名提交才能方便测试, 这里用 tree 这个小应用来测试
wget -O /tmp/tree_2.2.1-4_amd64.deb https://mirrors.ustc.edu.cn/debian/pool/main/t/tree/tree_2.2.1-4_amd64.deb
dpkg-sig --sign builder -k 906B7B5387FA4D2C445795D737B153163F339E56 /tmp/tree_2.2.1-4_amd64.deb

# 最后推送到镜像源目录
sudo -u www-data reprepro --basedir /data/mirror/debian includedeb noble /tmp/tree_2.2.1-4_amd64.deb
# 这里会报错异常, 但是包已经成功导入:
# ebian includedeb noble /tmp/tree_2.2.1-4_amd64.deb
# Exporting indices...
# gpgme gave error GPGME:1: General error
# ERROR: Could not finish exporting 'noble'!
# This means that from outside your repository will still look like before (and
# should still work if this old state worked), but the changes intended with this
# call will not be visible until you call export directly (via reprepro export)
# Changes will also get visible when something else changes the same file and
# thus creates a new export of that file, but even changes to other parts of the
# same distribution will not!
# There have been errors!
# 这里能看到包已经导入, 但是实际上是因为签名异常
sudo -u www-data reprepro --basedir /data/mirror/debian ls tree
# 这里涉及到其实就是权限问题, 因为生成的 gpg 默认只生成加载当前的登陆用户
# 我们看到之前处理的都是 www-data 系统用户操作, 而我们一直都是直接运行生成,
# 私钥公钥都是放置在 '/home/{目前操作管理员用户}/.gnupg/openpgp-revocs.d/'
# www-data 是没办法跨权限目录去加载和签名这些私钥公钥
# 所以这里应该做的是重新构建个系统账号去处理这些专门操作, 而且必须要有以下条件:
# 1. HOME 目录文件夹存在
# 2. 追加 www-data 用户组

因为这边一般是内网处理, 所以都是干脆 www-data 挂载暴露, 后续这里的操作和搭建一致只是需要创建系统管理账号

注意: gpg 工具集版本变动很频繁, 有的参数和接口随着版本不同而变动得很频繁, 这里部署方式可能不是很准确.

国内的镜像站则是直接同步外包资源到本地, 不会自己重新签名所以签名这些信息依旧还是官方的