自定义 yum 软件源:Linux 上 createrepo,Windows 上纯 Python 也能造

内网机器连不上公网、某批 rpm 要反复装十几台机器、官方源突然改地址------这三种情况都指向同一件事:自己造一个 yum 源。这篇先讲清一个源到底由什么组成,再讲最容易卡住的一步------包和依赖怎么凑齐(拿 docker-ce 的 29 个依赖做实例),然后给 Linux 侧 createrepo 的五步做法,最后给 Windows 侧的做法:不装 createrepo,用一个纯 Python 脚本直接生成元数据,客户端用起来和官方源没有任何区别。

本文概要:

项目 内容
文章主题 自建 yum 软件源:依赖闭包与离线下载、目录规范与 repodata 元数据、Linux 侧 createrepo 与 Windows 侧纯 Python 生成、HTTP 托管与客户端 .repo 配置
所属类别 Linux 运维 → 软件包管理 → 自定义仓库
知识关键字 createrepo · repodata · repomd.xml · primary.xml.gz · baseurl · gpgcheck · yum makecache · repotrack · --downloadonly

主线是四件事:凑齐包 → 摆好目录 → 生成清单 → 托管出去 。Linux 和 Windows 两条路的差别只在第二步用的工具不同,产物完全一样;客户端那边只认 baseurl 指向的 repodata/repomd.xml,不问你是怎么造出来的。

一、一个源由什么组成

yum 源不是「一个放着 rpm 的共享目录」。它必须包含两样东西:

  1. rpm 包本身;
  2. 一份描述这些包的清单 ,放在 repodata/ 目录里。

只放 rpm 不放清单,客户端会报 Cannot find a valid baseurl for repo 或 repomd.xml: HTTP Error 404------它找不到清单,就认为这个地址不是源。

清单由四个文件组成:

文件 内容 缺了会怎样
repomd.xml 入口,记录另外三个文件的文件名、校验和、大小 直接报 404 或校验失败
<hash>-primary.xml.gz 每个包的 name / arch / version / checksum / 依赖关系 无法算依赖
<hash>-filelists.xml.gz 包内文件清单 yum provides /usr/bin/xxx 失效
<hash>-other.xml.gz 变更日志等附加信息 yum 会告警,一般不影响装

文件名前面的 hash 由文件内容算出。内容一变,hash 就变 ------这正是 yum 判断「本地缓存过期没有」的依据,也是为什么改完源必须让客户端 yum clean all。

二、Linux 侧:createrepo 五步

2.1 摆好目录

bash 复制代码
[root@docker ~]# mkdir -p /mnt/repo/Packages
# 把要提供的 rpm 全拷进去(依赖包也得在,见第三节)
[root@docker ~]# cp /root/rpms/*.rpm /mnt/repo/Packages/
[root@docker ~]# ls /mnt/repo/Packages | wc -l
62

Packages/ 这个名字不是强制的,但全部堆在根目录会很难维护,社区惯例是单开一层。createrepo 会递归扫描,放哪一层都能识别。

2.2 装 createrepo

bash 复制代码
[root@docker ~]# yum install -y createrepo

离线环境 要手工把依赖补齐。createrepo 需要这四个包,缺一个都装不上:

bash 复制代码
[root@docker ~]# ls /root/createrepo-deps/
createrepo-0.9.9-28.el7.noarch.rpm
deltarpm-3.6-3.el7.x86_64.rpm
libxml2-python-2.9.1-6.el7.5.x86_64.rpm
python-deltarpm-3.6-3.el7.x86_64.rpm

# 一条命令一起装,让 rpm 自己解依赖顺序
[root@docker ~]# rpm -Uvh /root/createrepo-deps/*.rpm

2.3 生成元数据

bash 复制代码
[root@docker ~]# createrepo /mnt/repo
Spawning worker 0 with 62 pkgs
Workers Finished
Saving Primary metadata
Saving file lists metadata
Saving other metadata
Generating sqlite DBs
Sqlite DBs complete

跑完多出一个 repodata/ 目录,里面除了三个 XML,还有 sqlite.bz2 版本(老版本 yum 用来加速查询)。

2.4 加了新包之后:增量更新

bash 复制代码
[root@docker ~]# createrepo --update /mnt/repo

--update 只处理新增或有变化的包,比全量重建快得多。加了包不重新跑这一条,新包在客户端永远看不到------这是自建源最常见的「装了没生效」。

2.5 托管出去:四种方式

方式 baseurl 写法 适用
本地目录 baseurl=file:///mnt/repo 只给本机用,如光盘源
Python 临时服务 baseurl=http://10.1.8.102:8000 临时验证,一条命令就起
nginx baseurl=http://10.1.8.102/repo 生产常用,支持并发与限速
httpd baseurl=http://10.1.8.102/repo 已有 Apache 环境时顺手用

最省事的临时验证:

bash 复制代码
[root@docker ~]# cd /mnt && python -m http.server 8000

nginx 只需要一个 root 指向:

nginx 复制代码
server {
    listen 80;
    server_name 10.1.8.102;
    location /repo {
        alias /mnt/repo;
        autoindex on;
    }
}

📌 alias 后面不要 漏掉结尾的斜杠匹配规则:写成 location /repo/ { alias /mnt/repo/; } 更稳,否则 /repo 少一层斜杠会 404。

2.6 客户端配置与验证

bash 复制代码
[root@docker-cli ~]# cat > /etc/yum.repos.d/local-repo.repo <<'EOF'
[local-repo]
name=Local Repo from docker
baseurl=http://10.1.8.102/repo
enabled=1
gpgcheck=0
EOF

[root@docker-cli ~]# yum clean all
[root@docker-cli ~]# yum makecache
local-repo                                | 3.6 kB  00:00:00
(1/4): local-repo/group_gz                | 153 kB  00:00:00
(2/4): local-repo/primary_db              | 3.3 MB  00:00:00
元数据缓存已建立

yum makecache 成功就等于源通了。再确认能查到包:

bash 复制代码
[root@docker-cli ~]# yum list available | grep docker-ce
docker-ce.x86_64            26.1.4-1.el7            local-repo

⚠️ 注意 :依赖包必须一起放进源里。只放 docker-ce 一个包,客户端装的时候会去别的源找 container-selinux、selinux-policy 这些依赖;那台机器没有外网,就报 requires xxx, but none of the providers can be installed。到底要放哪些包、怎么一次下全、怎么验证一个不缺,是下一节的全部内容。

三、把包凑齐:docker-ce 的 29 个依赖怎么来

上一节第一步写「依赖包也得在」,这五个字落地时最容易卡住。自建源装不上,九成不是清单写错,是包少了一个。这一节拿 docker-ce 26.1.4 从零走一遍:怎么算清要下哪些、用哪条命令下、怎么验证一个都不缺。

3.1 先算清依赖闭包

一个包会要求别的包,被要求的包又要求别的,递归到底得到的完整集合叫依赖闭包 。它才是你要凑齐的东西------不是你在 yum install 后面写的那两个包名。

29 个包按「谁提供」算账,是这样三档:

来源 包数 体积 典型包
docker-ce.repo 6 113 MB 含你要装的 docker-ce 27 MB 和 cli 15 MB,另有 containerd.io 35 MB、buildx 14 MB、compose 13 MB、rootless 9 MB
CentOS 7 base / extras 23 10 MB container-selinux、selinux-policy、policycoreutils-python、iptables、libcgroup
CentOS 7 minimal 自带 --- 不用下 glibc、systemd、bash、python 2.7、tar、sed、sqlite

顺带解释一个容易看糊涂的地方:E:\repo\docker\Packages 里其实有 62 个包,多了 33 个 php、nginx、redis 相关的东西。一个源放几个软件栈都行,客户端只按包名取,不会因为同目录有 php 就把 docker 装错。这 29 个只是 docker-ce 这一条依赖链上的量。

两个反直觉的地方值得单独说。

一是那三个插件包跑不掉。 docker-ce-cli 对 docker-buildx-plugin 和 docker-compose-plugin 的依赖在 rpm 头里标的是强制(flags 为 0x0,不是 Recommends),所以 buildx 和 compose 加起来 26 MB,你不想装也得下------占了总量的五分之一。

二是 docker-ce 与 rootless-extras 互为依赖。 docker-ce 要求 docker-ce-rootless-extras,后者又反过来要求 docker-ce,形成环。环本身 rpm 能处理,但下载工具遇到环容易截断 :只写 docker-ce 去下,可能拿到一个缺 rootless 的半套。两个一起写在命令行里就没有这个问题。

3.2 三条下载命令,差别在「看不看本机已装」

命令 来自 下依赖 看本机已装 适合
yumdownloader --resolve yum-utils 是,但只解析一层 会 临时补一两个包
yum install --downloadonly yum 自带 是,走完整解析 会 主力方案,配干净机器
repotrack yum-utils 是,整棵依赖树 不看 一次下全,不怕多

三条命令的正经写法:

bash 复制代码
# 方式一:yumdownloader,必须带 --resolve,否则只下主包
[root@docker ~]# yumdownloader --resolve --destdir=/root/rpms docker-ce docker-ce-cli

# 方式二:yum 自带,走完整解析
[root@docker ~]# yum install --downloadonly --downloaddir=/root/rpms docker-ce docker-ce-cli

# 方式三:repotrack,不看本机状态
[root@docker ~]# repotrack -p /root/rpms docker-ce docker-ce-cli

前两条有个共同的坑:它们按「本机缺什么」来决定下什么 。本机已经装过 container-selinux,它就不会下------可你要带走的是给干净机器用的。所以真正的前提是下一节那件事:找一台什么都没装过的机器去下。

3.3 拿容器当下载机

最干净的下载机是一个刚拉起来、什么都没装过的 centos:7 容器:

bash 复制代码
[root@docker ~]# mkdir -p /root/rpms
[root@docker ~]# docker run -it --rm -v /root/rpms:/rpms centos:7 bash

# 容器里:先确认版本和目标机一致
[root@3f9a1c7b2d10 /]# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)

[root@3f9a1c7b2d10 /]# yum install -y yum-utils
[root@3f9a1c7b2d10 /]# yum-config-manager --add-repo \
                    https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

[root@3f9a1c7b2d10 /]# yum install --downloadonly --downloaddir=/rpms \
                    docker-ce docker-ce-cli

[root@3f9a1c7b2d10 /]# ls /rpms | wc -l
29

exit 出来,29 个包已经在宿主机的 /root/rpms 里。容器是全新环境,--downloadonly 找不到任何「已装」记录,不会有包被跳过。

版本必须对齐。 下载机和目标机的小版本要一致,7.9 对 7.9。差一个小版本就可能出现下载机拉的包要求 glibc 2.17-317、而目标机只到 2.17-307,装的时候报 Requires: libc.so.6(GLIBC_2.14)(64bit) 这种看起来像「系统太老」的错。目标机如果是 7.6,就用 centos:7.6.1810 的镜像,或者在下载命令上加 --releasever=7.6.1810。

3.4 断网装一遍,才算凑齐

包下完不等于凑齐。唯一可靠的验证是在一台连不上任何源的机器上装一遍:

bash 复制代码
[root@docker ~]# docker run -it --rm --network none -v /root/rpms:/rpms centos:7 bash

# 容器里只留一个本地源
[root@b7e2d90f1a4c /]# cat > /etc/yum.repos.d/local.repo <<'EOF'
[local]
name=Local test
baseurl=file:///rpms
enabled=1
gpgcheck=0
EOF

[root@b7e2d90f1a4c /]# yum --disablerepo='*' --enablerepo=local install -y docker-ce
Installed:
  docker-ce.x86_64 3:26.1.4-1.el7
Dependency Installed:
  container-selinux.noarch 2:2.119.2-1.911c772.el7_8
  containerd.io.x86_64 1.6.33-3.1.el7
  ...
Complete!

两个参数是关键:--network none 让容器物理断网 ,--disablerepo='*' 把容器自带的源全关掉。这样 yum 只剩 file:///rpms 一个来源,能装完就证明这 29 个包互相自洽、一个不缺。

这一步顺带把清单也验了:repodata 如果是坏的(XML 截断、校验和对不上),这里会先报解析错或 404,根本走不到装包。

3.5 还是缺包:按报错形态反查

报错长这样 缺的是什么 怎么找
Requires: container-selinux >= 2:2.9 某个包 yum provides 'container-selinux'
Requires: /usr/bin/docker-init 某个文件 yum provides /usr/bin/docker-init
Requires: libc.so.6(GLIBC_2.14)(64bit) 库的版本 yum provides '*libc.so.6*'

第三种最容易误判:查得到提供者、但装上还是不满足。那不是缺包,是下载机和目标机版本不一致------回去按 3.3 对齐版本重下。

📌 yum provides 依赖 filelists.xml.gz,而第四节那个纯 Python 脚本造的源里没有文件条目,所以拿它查文件会查不到。查包名和库名不受影响,因为它们写在 primary.xml.gz 里。

3.6 打包带到内网

bash 复制代码
[root@docker ~]# cd /root && tar czf docker-ce-offline-26.1.4.tar.gz rpms/
[root@docker ~]# ls -lh docker-ce-offline-26.1.4.tar.gz
-rw-r--r-- 1 root root 126M Nov  5 14:20 docker-ce-offline-26.1.4.tar.gz

128 MB 压完 126 MB------rpm 内部已经是压缩过的包,别指望 tar 再瘦身。

到了内网,把 29 个 rpm 丢进 E:\repo\docker\Packages\,双击 update-repo.bat 重建清单,这条链就闭合了。四个环节分别在哪台机器做:

环节 机器 做什么
下载 有外网的 CentOS 7(容器即可) 三条命令任选,下全 29 个
验证 同一台,断网 --network none 装一遍
建源 Windows 或内网 Linux 拷进 Packages/,跑生成脚本
使用 内网客户端 配 .repo,yum install

一句话收这一节:判断一个源做没做对,不看 repodata 里有几个文件,看断网机器能不能装上。

四、Windows 侧:没有 createrepo 也能造

createrepo 是 Linux 工具,Windows 上没有。但元数据格式是公开的------只要按格式生成 XML,客户端就认。这条路适合:只有 Windows 机器、不想为造一次元数据装一堆依赖、或者要放在共享盘上随时重建。

4.1 先看已经跑起来的那套布局

一个实际在用的结构长这样:

复制代码
E:\repo\
├── docker\                      ← 一个源(对应一个 baseurl)
│   ├── Packages\                ← 62 个 rpm
│   │   ├── docker-ce-26.1.4-1.el7.x86_64.rpm
│   │   ├── docker-ce-cli-26.1.4-1.el7.x86_64.rpm
│   │   ├── containerd.io-1.6.33-3.1.el7.x86_64.rpm
│   │   ├── container-selinux-2.119.2-1.911c772.el7_8.noarch.rpm
│   │   └── ...
│   └── repodata\                ← 脚本生成的清单
│       ├── repomd.xml
│       ├── primary.xml.gz
│       ├── filelists.xml.gz
│       └── other.xml.gz
├── createrepo-deps\             ← 四个离线依赖包,给 Linux 侧备用
├── make_repodata.py             ← 生成器本体(单文件,只用标准库)
├── update-repo.bat              ← Windows 上一键重建
├── update-repo.sh               ← Linux 上一键重建(优先用 createrepo)
├── docker-ce.repo               ← 公网源(对照用)
└── docker-ce-network.repo       ← 客户端模板(内网源)

一个子目录 = 一个源。 想再加一个 PHP 源,就再建 E:\repo\php\Packages\,各自有独立的 repodata,客户端配两个 .repo 段即可。

4.2 生成器做了什么

make_repodata.py 的核心动作只有三步:

  1. 解析 rpm 的二进制头 :先读 lead 魔数 \xed\xab\xee\xdb,跳过签名头,再定位主 header。然后按 tag 取值,拿到 name、version、arch、依赖、provides 等字段;
  2. 拼出三个 XML :primary.xml.gz(含 rpm:provides / rpm:requires)、filelists.xml.gz、other.xml.gz;
  3. 算校验和写 repomd.xml:每个 gz 的 sha256、解压后大小、时间戳,一项都不能少。

用法就一条:

cmd 复制代码
python make_repodata.py E:\repo\docker

输出:

复制代码
found 62 rpms
  + docker-ce-26.1.4-1.el7.x86_64.rpm epoch=0
  + docker-ce-cli-26.1.4-1.el7.x86_64.rpm epoch=0
  ...
OK -> E:\repo\docker\repodata

⚠️ 注意 :这个纯 Python 实现有意省掉了 filelists.xml.gz 里的文件条目(只写了包名和版本),所以 yum provides /usr/bin/xxx 在它造的源上查不出东西 。装包、算依赖都不受影响------因为那两件事只靠 primary.xml.gz。如果确实需要 provides,就在任意一台 Linux 上用 createrepo 重建一次,把整个 repodata/ 拷回来。

4.3 一键重建

Windows 上双击即可:

bat 复制代码
@echo off
rem update-repo.bat -- rebuild yum repodata for the E:\repo\docker source
set "REPO=%~dp0docker"
where python >nul 2>nul
if errorlevel 1 (
    echo [ERROR] python not found in PATH
    pause
    exit /b 1
)
python "%~dp0make_repodata.py" "%REPO%"
if errorlevel 1 (
    echo [ERROR] repodata generation FAILED
) else (
    echo [OK] repodata updated in docker\repodata\
)
pause

Linux 上跑的是同名 .sh,逻辑更聪明一点:有 createrepo 就优先用它,没有才回落到 Python 脚本。

bash 复制代码
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="${1:-$SCRIPT_DIR/docker}"
if command -v createrepo >/dev/null 2>&1; then
    createrepo --update "$REPO_DIR"
elif command -v python3 >/dev/null 2>&1 || command -v python >/dev/null 2>&1; then
    python3 "$SCRIPT_DIR/make_repodata.py" "$REPO_DIR" 2>/dev/null || \
    python "$SCRIPT_DIR/make_repodata.py" "$REPO_DIR"
else
    echo "[错误] 没有 createrepo 也没有 python"
    echo "       可先装离线包: rpm -Uvh $SCRIPT_DIR/createrepo-deps/*.rpm"
    exit 1
fi

3.4 把 E:\repo 发布出去

Windows 上同样要起一个 HTTP 服务,把 E:\repo 映射成 /repo。三种做法任选:

做法 命令 说明
Python 临时服务 cd /d E:\repo && python -m http.server 80 最快,关掉窗口就停
nginx for Windows root 指向 E:/repo,location /repo { alias E:/repo; } 常驻,支持并发
已有 IIS 新建站点,物理路径 E:\repo 适合已经是 Windows 服务器的环境

起完在浏览器访问 http://10.1.8.100/repo/docker/repodata/repomd.xml,能看到 XML 内容就说明托管通了。

4.5 客户端配置模板

bash 复制代码
# 把 <SERVER_IP> 换成 Windows 那台的地址,例如 10.1.8.100
[root@docker-cli ~]# cat > /etc/yum.repos.d/docker-ce-local.repo <<'EOF'
[docker-ce-local]
name=Docker CE Local
baseurl=http://10.1.8.100/repo/docker
enabled=1
gpgcheck=0
EOF

[root@docker-cli ~]# yum clean all
[root@docker-cli ~]# yum makecache

同时把原来的公网 docker-ce 源挪走,避免两个源抢同一个包:

bash 复制代码
[root@docker-cli ~]# mkdir -p /etc/yum.repos.d/bak
[root@docker-cli ~]# mv /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/bak/

4.6 验收:真的装上

bash 复制代码
[root@docker-cli ~]# yum install -y docker-ce
 Installing:
  docker-ce          x86_64  3:26.1.4-1.el7            docker-ce-local   28 M
 Installing for dependencies:
  containerd.io      x86_64  1.6.33-3.1.el7            docker-ce-local   37 M
  ...

[root@docker-cli ~]# docker --version
Docker version 26.1.4, build b87b14e

Installing for dependencies 里那些包全都来自 docker-ce-local------说明依赖也一起被源覆盖了,这台机器不需要外网。

五、给源加签名(可选)

gpgcheck=0 省事,但等于放弃校验。要开校验就得对 repomd.xml 签名:

bash 复制代码
# 1) 在源服务器上生成密钥(一路回车即可,保护密码可以留空)
[root@docker ~]# gpg --gen-key

# 2) 对 repomd.xml 做分离签名,生成 .asc
[root@docker ~]# gpg --detach-sign --armor --output /mnt/repo/repodata/repomd.xml.asc \
                    /mnt/repo/repodata/repomd.xml

# 3) 导出公钥,放到客户端的信任目录
[root@docker ~]# gpg --export -a "zhb repo" > /etc/pki/rpm-gpg/RPM-GPG-KEY-local

客户端改成:

ini 复制代码
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-local

之后每次 createrepo --update 都要重新签一次 ,否则签名对不上,报 repomd.xml signature could not be verified。

六、排错速查

报错 / 现象 原因 处理
Cannot find a valid baseurl for repo: xxx baseurl 写错、服务没起、或防火墙挡了 浏览器直接访问 baseurl 验证
repodata/repomd.xml: HTTP Error 404 repodata/ 没生成,或不在 baseurl 那一层 确认 repomd.xml 在 <baseurl>/repodata/ 下
repomd.xml parser error XML 生成不完整(脚本被中断) 重跑生成脚本
requires xxx, but none of the providers can be installed 依赖包没进源 在能装的机器上抓全 Installing: 列表
加了新包,客户端看不到 没重新生成元数据 createrepo --update 或重跑脚本,再 yum clean all
客户端仍用旧缓存 yum 缓存没清 yum clean all && yum makecache
repomd.xml signature could not be verified 更新后没重新签名 重新 gpg --detach-sign

⚠️ 注意 :客户端和源服务器的时间差太大 也会导致校验失败(HTTP 校验带 If-Modified-Since)。内网机器记得对时,或直接把 gpgcheck 关掉先跑通再开。

📚 参考资料(官方文档)

附:命令英文全称速查表

命令 / 名词 英文全称 中文
createrepo create repository 创建仓库(生成元数据)
repodata repository data 仓库数据(清单目录)
repomd repository metadata 仓库元数据入口
baseurl base URL 基准地址
gpgcheck GNU Privacy Guard check 签名校验开关
makecache make cache 建立元数据缓存
rpm RPM Package Manager RPM 包管理器
alias --- 路径别名(nginx 指令)

自建源这件事,本质上只要求你凑齐两样:包 和清单 。清单的生成工具可以换(createrepo 或一个 Python 脚本),托管方式可以换(nginx、httpd、Python 的 http.server、共享盘挂载),客户端只认 <baseurl>/repodata/repomd.xml 这一条路径。换包必须重建清单,换地址必须改客户端 .repo,其余都是排错。

相关推荐
武雄(小星Ai)15 分钟前
Ollama报llama-server binary not found排坑实录:服务活着引擎没了
人工智能·windows·大模型·ollama·排坑
Android系统攻城狮17 分钟前
Linux FFmpeg深度解析之avio_open调用流程与实战(三十八)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
独隅24 分钟前
WebStorm Linux 版安装实战与核心效能展示
linux·ide·webstorm
瑞克-桑切斯2 小时前
ERP 企业生产管理系统部署手册
运维·docker·云原生·容器·eureka
江屿风2 小时前
【Git-组件】【Git入门:从文档灾难到版本掌控】流食般投喂
运维·笔记·git·组件·版本控制
我不会起名字3222 小时前
K8s 节点磁盘打满导致 Pod 被驱逐:4 类占用与回收顺序
运维·kubernetes·k8s·diskpressure·evicted
雪落漂泊3 小时前
Linux进程(五):进程地址空间篇
linux·运维·服务器
瑞克-桑切斯3 小时前
docker mail商城
运维·docker·容器
AR-26710-3 小时前
Linux Day32——网络,ROCE-IB与以太网
linux·运维