1. 项目概述与核心需求解析
1.1 这个项目的真实痛点是什么
先直接说结论:Linux 内网环境装软件,最痛苦的不是装不上,而是你压根不知道“缺什么”“去哪找”“怎么传进去”。外网环境下一条apt install或者yum install就解决的事,到了内网,就变成了一场“依赖考古”加“文件搬运”的苦力活。我最早接手这类任务时,天真的以为只要把安装包拷进去就能完事,结果被依赖链、版本冲突、架构不匹配轮番教做人。
这个项目的核心场景,是那些不允许连接公网的生产环境、涉密机房、政企内网、以及某些隔离的研发测试区。这些地方不是你不想上网,而是安全策略不允许。机器上可能只有一个纯净的 Linux 系统,连最基本的编译工具链都没有,你手里可能只有一台能上网的跳板机或者自己的笔记本,所有软件都只能靠人工搬运进去。这时候,“安装程序”这件事就从“执行一条命令”退化成了“全套物流+装配”的流程。
1.2 适合谁看、能解决什么问题
这篇内容适合以下几类人:
- 刚接手内网 Linux 服务器运维的工程师,还没踩过“缺依赖”的坑。
- 需要在内网部署业务系统的实施人员,比如要装数据库、中间件、Python 环境等。
- 做私有化交付的开发者,代码写完了,结果发现客户现场不能联网,环境都配不起来。
- 想系统搞懂 Linux 包管理机制和依赖解析原理,而不是只会敲命令的新手。
读完你会掌握:内网环境下从“确定需要什么”到“把软件装好”的完整闭环,包括离线仓库搭建、单包搬运、源码编译、容器镜像直传这几种主流方案,以及我实际踩过的坑和排查套路。
2. 方案选型:先想清楚再动手
2.1 四种主流做法和它们的适用边界
内网装软件没有银弹,先看场景再选方案。我把常见做法分成四类,各有各的脾气。
第一种:离线软件仓库镜像。适合要装大量软件、且目标机器数量多的情况。比如你维护着几十台内网 CentOS 服务器,不可能一台一台拷 rpm,那就得在一台能联网的机器上用工具把整个仓库同步下来,再搬到内网做本地源。这个方案一劳永逸,但前期准备时间最长,而且仓库体积动不动几个 GB 甚至几十 GB,搬运介质要想清楚。
第二种:单包加依赖树搬运。适合只装一两个软件的场景。比如内网要装一个 Nginx,那就找到 Nginx 的 rpm 包,再把它的所有依赖项挨个找出来,一起拷进去。这个方案灵活,但是依赖关系经常有嵌套,手动一个个找容易漏,拷进去才发现缺一个,来回折腾。
第三种:源码编译安装。适合没有现成二进制包、或者需要自定义编译参数的场景。在内网机器上只要有编译工具链(gcc、make 等),就能从源码包开始编译。问题是内网机器往往连编译工具链都没有,这时候就需要在能联网的机器上把工具链等所有依赖打包好,先解决“装编译器”的问题。
第四种:容器镜像离线导入。这是我在近两年最推荐的方式。内网机器只要能跑 Docker,就用docker save把镜像从外网导出来,拷贝进内网后用docker load导入,然后跑容器。镜像本身自带完整的运行环境和依赖,几乎不存在“还缺某个 so 库”的问题。很多商业化私有化交付,现在都是直接交付一个镜像 tar 包。
2.2 我为什么说“先断网再想方案”
这里有个反直觉的经验:不要一上来就问“内网怎么装 XXX”,而是先问“我的内网到底断到什么程度”。有的内网只是没有公网路由,但 DNS 还能用;有的则是完全物理隔离,连 USB 接口都被封了,只能通过特定的摆渡系统传文件。这两种情况,方案完全不同。
物理隔离环境,你只能依赖光盘、摆渡机、或者审批后的临时网络通道。这时候体积越小越好,我建议优先考虑容器镜像(如果可以接受 Docker 的话)或者源码包加依赖裁剪,而不是去搬整个仓库。
只禁外网但允许内网互通的环境,你可以在内网搭一台源服务器,把离线仓库放在那,其他机器通过内网 HTTP 访问。这样后续补包、升级都方便,不用每台机器都拿 U 盘去插。
我自己吃过一次亏:当时觉得内网机器“反正能插 U 盘”,就图省事直接拷了一堆 rpm 进去,结果装到一半发现还缺一个 Perl 模块,而 U 盘已经在另一个城市了。后来痛定思痛,只要机器超过 3 台,我必搭本地源。
3. 实操准备:把“外网”变成“内网补货渠道”
3.1 确定目标机器的系统信息和架构
动手前第一件事,不是找安装包,而是确认目标机器的“身份证”。需要收集的信息包括:
- 发行版和版本:比如 CentOS 7.9、Ubuntu 20.04、openEuler 22.03。
- 内核架构:x86_64、aarch64、armv7l 等。这个太容易踩坑了,我见过有人在 aarch64 的机器上硬装 x86_64 的 rpm,报错报得莫名其妙。
- 包管理器:yum/dnf、apt、zypper、pacman,不同发行版用的包格式和管理器完全不同。
- 关键运行库现状:比如 glibc 版本,这决定了你带的二进制包能不能跑起来。很多软件对 glibc 版本有硬性要求,低了直接报
version GLIBC_2.28 not found。
确认命令我贴一下,方便直接抄:
cat /etc/os-release uname -m ldd --version which yum || which dnf || which apt-get拿到这些信息之后,把所有要用的安装包都按“发行版+架构”来归档命名,避免混用。我习惯建一个目录结构:
~/offline-pkgs/ ├── centos7-x86_64/ ├── ubuntu20.04-x86_64/ └── openeuler-aarch64/3.2 在能联网的机器上准备下载工具
内网机器没网,但你总得有一台能上网的电脑(哪怕是临时用的虚拟机)。这台“下载机”的任务是:把目标机器需要的所有软件包,连同依赖,全部拉下来。
以 CentOS/RHEL 系为例,最常用的工具是yumdownloader或者dnf download,它们可以只下载而不安装。但要注意,这两个工具默认不拉依赖,需要加--resolve参数。我之前用yumdownloader --resolve时踩过一个坑:它会解析当前系统的依赖,如果下载机的系统和目标机系统版本不一致,比如下载机是 CentOS 8,目标机是 CentOS 7,解析出的依赖就可能对不上。所以最稳妥的玩法是:用 Docker 起一个和目标机器同版本的系统容器,在容器里执行下载命令,这样解析出来的依赖才是准的。
Ubuntu/Debian 系则用apt-get download配合apt-rdepends递归解析依赖,或者直接用apt-cache depends --recurse生成依赖列表,再用apt-get download批量拉取。
3.3 核心实操:构建 CentOS 7 离线 yum 仓库
这是我最常用的方案,直接给完整过程。假设目标机是 CentOS 7.9 x86_64,需要离线安装 nginx、postgresql、python3 这些常用软件。
先在下载机上准备一个工作目录,并创建一个同版本容器:
mkdir -p /data/centos7-repo docker run -itd --name centos7dl centos:7.9.2009进入容器,配置好网络源(容器里默认就能联网),然后用repotrack工具把软件包和全部依赖下载下来。repotrack比yumdownloader更彻底,它会下载所有依赖,包括那些“已经装过”的包,这样生成的仓库用下来最省心。
docker exec -it centos7dl bash # 在容器内 yum install -y yum-utils createrepo mkdir /pkgs # 下载软件包及其全部依赖 repotrack nginx postgresql-server python3 -p /pkgs下载完成后,把容器里的/pkgs目录拷出来:
docker cp centos7dl:/pkgs /data/centos7-repo/然后用createrepo生成仓库元数据:
cd /data/centos7-repo createrepo .拷到内网机器后,放到比如/opt/centos7-local-repo,并配置本地源:
cat > /etc/yum.repos.d/local.repo << EOF [local] name=Local CentOS 7 Repo baseurl=file:///opt/centos7-local-repo enabled=1 gpgcheck=0 EOF # 清缓存、重建缓存 yum clean all yum makecache之后内网机器上就可以正常用:
yum install -y nginx postgresql-server python3这条命令会从本地源解析依赖并安装,全网断网也能跑。这是我试过的最稳的内网批量装机方式。
4. 实操过程:从搬运到安装的完整链路
4.1 单包直装的细节与依赖坑
如果只是临时装一个小工具,不想建仓库,那就用“单包+依赖”的方式。以 CentOS 7 上离线安装tree命令为例,这软件本身依赖很少,很好演示。
在下载机容器里:
yumdownloader --resolve tree你会得到tree的 rpm 和它依赖的libselinux等几个 rpm。把它们拷到内网,然后:
rpm -ivh *.rpm这里有个经验:用rpm -ivh而不是rpm -Uvh,因为-i是全新安装,-U是升级,有时候-U会因为版本冲突而失败得很诡异。另外,如果目标机器上已经存在某些依赖库的老版本,rpm 可能会因为“被已安装的包冲突”而拒绝安装,这时候用--replacefiles或者--nodeps都得想清楚后果,我基本只在临时救急时才用--nodeps,而且装完一定要立即验证程序能不能跑,别装完就以为完事了。
验证方式最简单直接:
tree /usr/bin如果报错说缺 so 文件,就用ldd /usr/bin/tree看一下到底缺哪个库,再去补对应的包。
4.2 源码编译安装的完整流程
当二进制包找不到、或者包版本太老不满足需求时,就得源码编译。以内网装 Python 3.10 为例(很多内网机器自带的 Python 是 2.7 或者 3.6,装新版本时就很典型)。
先在下载机上下载 Python 3.10 源码包并解压,还要看一下它的编译依赖。Python 编译需要 gcc、make、zlib-devel、libffi-devel、openssl-devel 等,这些在目标机器上可能没有,且 no 网不好装。我一般把下载机的/usr/lib64下相关的 rpm 一并拷过去,或者在容器里用之前说的仓库方式准备好。
在内网机器上:
tar xf Python-3.10.12.tgz cd Python-3.10.12 ./configure --enable-optimizations --prefix=/usr/local/python3.10 make -j$(nproc) make install这里需要注意,如果编译过程中报ModuleNotFoundError: No module named '_ssl',基本就是没装 openssl-devel。老式 configure 检测不到头文件,就直接把 ssl 模块跳过了。解决方法是把 openssl-devel 装上(或者指定 openssl 路径)再重新 configure、make clean 后重来。我当时在这个坑里卡了一整天,一定要记得make clean一下再重编,直接再次 make 往往不生效。
编译完成的 Python 还要处理软链:
ln -s /usr/local/python3.10/bin/python3.10 /usr/local/bin/python3 ln -s /usr/local/python3.10/bin/pip3.10 /usr/local/bin/pip3然后验证:
python3 -V python3 -c "import ssl; print('ssl ok')"4.3 离线安装 pip 包与 wheel 机制
内网里用 Python 的人最常问“pip install 不了怎么办”。其实思路很清晰:在下载机的 pip 缓存目录把所有需要的包下载成 wheel,然后搬到内网装。
用pip download指令,指定--no-deps或者让它把依赖都拉全:
mkdir /data/pip-pkgs cd /data/pip-pkgs pip download requests pandas scikit-learn -d ./ --platform manylinux2014_x86_64 --python-version 3.10 --only-binary=:all:参数里--platform和--python-version是指定目标平台的 wheel,避免下载到别的架构的包。如果目标机器上没有编译环境,就一定要加--only-binary=:all:,否则 pip 可能给你下载源码包,到内网就没法编译了。
然后把这些.whl文件拷到内网,执行:
pip install --no-index --find-links=/data/pip-pkgs requests pandas scikit-learn仍有个小坑:某些纯 Python 包不提供 manylinux wheel,只有源码包(sdist)。遇到这种,我可以接受在内网编译(那得先保证编译依赖都在),或者找一个临时方案绕过去。我的建议是,把--only-binary=:all:和--no-binary=:all:都用上,分两轮下载,轮轮检查,确保没漏。
4.4 容器镜像离线导入是“真香”方案
如果你在内网里能接受容器,那么离线装软件这件事的难度直接下降一个量级。举个实际例子:内网要部署一个 Redis,你不需要找个 rpm,也不用管 glibc 版本,只需要:
在外网机器上:
docker pull redis:7.2.4 docker save -o redis-7.2.4.tar redis:7.2.4把redis-7.2.4.tar拷进内网后:
docker load -i redis-7.2.4.tar docker run -d --name redis --restart=always -p 6379:6379 redis:7.2.4完事。镜像里面自带所有动态库、配置、可执行文件。这也是为什么现在越来越多私有化交付要求必须有 Docker 环境——因为隔离性太适合内网了。
需要注意的一点是镜像体积和 Docker 版本兼容性。有的老内网机器上 Docker 版本很低,用新机器docker save出来的镜像load可能会报 “the image version is not supported”。所以尽量在下载机上用和目标环境一致或更老的 Docker 版本导出,或者用docker export/import导出容器文件系统绕过这个限制(但会丢失镜像历史层和配置)。
5. 常见问题与排查技巧实录
5.1 典型报错速查表
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
No package xxx available | 本地源里没有这个包,或者源没配好 | 执行yum list | grep xxx确认包名和源;检查 local.repo 的路径和createrepo是否执行 |
package xxx requires yyy, but none of the providers can be installed | 依赖缺失 | 回到下载机用repotrack重新拉取;检查是不是有多个版本的 yyy 冲突 |
error: Failed dependencies | rpm 方式安装时依赖不满足 | 用rpm -qR xxx.rpm查看依赖列表,逐一确认;或者放弃 rpm 直装改用 yum localinstall |
libssl.so.1.1: cannot open shared object file | 程序运行时报缺库 | 用ldd 程序路径定位缺失库,然后找到对应库的包,安装进去 |
/lib/ld-linux-aarch64.so.1: No such file or directory | 二进制包架构不匹配 | 确认uname -m,下载对应架构的包 |
_ssl module failed to import/No module named '_ssl' | Python 编译时缺少 openssl-devel | 装上 openssl-devel 和 libffi-devel,make clean后重新 configure 再编译 |
5.2 我在内网装软件时常犯的错
第一,忽略 glibc 版本。我前期最常犯的就是这个。内网机器大多是 CentOS 7 或老版本 Ubuntu,自带的 glibc 版本很低。很多新版二进制都是动态链接到更高版本的 glibc,比如有些工具要求GLIBC_2.28,CentOS 7 只有2.17,直接跑就是报version GLIBC_2.28 not found。这时候要么找兼容旧 glibc 的版本,要么用容器方案,实在不行就得升级系统(但内网升级系统往往更麻烦)。
第二,下载机与目标机系统版本不一致。我用 Ubuntu 下载机给 CentOS 目标机拉包时,依赖解析完全是错误的。后来强制自己在 Docker 容器里模拟目标系统环境,这个问题就杜绝了。
第三,忘了处理 GPG 签名。内网源配置时,如果gpgcheck=1,而 rpm 包里带的是不同 GPG key,安装可能直接报错。我在内网环境图省事,直接把gpgcheck=0,尤其内网本地源,没毛病。
第四,不做安装后验证。装完不等于能跑,很多动态库问题是在运行第一步才暴露的。我固定给每个包写一个验证清单,比如nginx -t、python3 -c 'import numpy'、redis-cli ping,确保功能正常才算完。
5.3 一个真实排查案例:Nginx 装好了却起不来
某次在内网 CentOS 7 上装 Nginx,yum install nginx很顺利,nginx -v也正常,但是systemctl start nginx直接报Job for nginx.service failed。查journalctl -xe也看不出明显原因,最后看/var/log/nginx/error.log才发现是"/var/run/nginx.pid" failed (13: Permission denied)。原因很简单:内网机器上 SELinux 开着,而我手动改过 Nginx 的 pid 路径到/var/run/nginx-custom.pid,SELinux 阻止了 nginx 写入那个文件。
解决方法是把这个路径加到 SELinux 允许范围,或者干脆用回默认路径。排查思路分享一下:unit 失败先看systemctl status,再看/var/log/messages和对应的应用日志,最后怀疑 SELinux,用ausearch -m avc查拒绝记录。内网装了 SELinux 却不会调,是我遇到的最高频问题之一。
5.4 内网装软件前最好先做的三件事
- 列出完整依赖清单再动手。用工具把每个包递归依赖都列出来,形成文本清单,这个清单不仅是搬运依据,后期故障排查也能用。
- 在下载机上构建一个“搬家包”目录结构,按系统和架构分好,命名带完整版本号,避免到内网后“这个包是哪来的”都搞不清。
- 先跑通最小闭环。比如先只装一个最简单的
tree,走通“下载-搬运-安装-运行”的全链路,再铺开装其他软件。这样可以把运输、存储介质、权限问题提前暴露,免得大部队出发了才发现U盘格式不对。
6. 扩展思路:仓库离线化的进阶玩法
6.1 自己做离线 yum 源的最佳实践
前面提到用repotrack拉取一组包并createrepo,这里我补充一个更彻底的玩法:同步整个发行版的 Base 仓库。如果内网机器数量很多,且需要覆盖各种软件包,可以在一台能上网的机器上直接使用reposync把整个仓库同步下来。
yum install -y yum-utils createrepo mkdir -p /data/centos7-base reposync --repoid=base --repoid=extras --repoid=updates --download_path=/data/centos7-base createrepo /data/centos7-base这样拉下来可能有几十 GB,但在内网搭一个 HTTP 服务,比如 Nginx 或简单的python3 -m http.server 80,所有服务器都能用。我实际管过一个 40 台规模的内网集群,就是这么做源服务器的,后续再补包只需要更新一个目录,其他机器不用动。
6.2 Ubuntu/Debian 系离线源的构建
Ubuntu 系也可以用类似思路,但工具不一样。先在下载机上:
apt-get install -y apt-mirror mkdir -p /data/ubuntu-mirror编辑/etc/apt/mirror.list,只保留需要的发行版和组件,比如 focal main universe:
deb http://archive.ubuntu.com/ubuntu focal main universe clean http://archive.ubuntu.com/ubuntu然后执行:
apt-mirror同步完成后,/data/ubuntu-mirror/mirror/archive.ubuntu.com/ubuntu就是一个可用的仓库,拷贝到内网,在客户机配置:
cat > /etc/apt/sources.list << EOF deb [trusted=yes] file:///opt/ubuntu-mirror/mirror/archive.ubuntu.com/ubuntu focal main universe EOF apt-get update注意[trusted=yes]是为了跳过 InRelease 签名验证,内网环境下非常方便。
6.3 关于“源码包”的补充
有些软件连现成的二进制仓库都不存在,只能源码编译。前面说过了,编译工具链如果内网本来就没有,你最好在下载机上也准备好对应的工具链包。CentOS 7 可以用yum install gcc gcc-c++ make下载并打包这些 rpm,Ubuntu 上则是build-essential。这里有个小技巧:把编译工具链的 rpm 也放进你建的本地源里,装任何源码包之前先在目标机器上用本地源装好工具链,然后再源码编译。
编译参数的选择也别乱来,我习惯固定用--prefix=/usr/local/<软件名>-<版本>,这样卸载时直接删目录,不会污染系统默认路径。
7. 最终建议:结合场景选最省力的路
我自己总结了一套选择逻辑,实际项目里可以照着判断:
- 如果内网未来长期要装很多软件,首搭离线源,一劳永逸。
- 如果只是临时装几个包,用repotrack 拉依赖 + rpm/yum 本地安装最稳。
- 如果软件有官方二进制但依赖复杂,容器镜像是最优解。
- 如果任何现成二进制都没有,就用源码编译,但先把工具链问题解决掉。
无论选哪种,一定记住一个底层原则:内网安装工作的核心不在“安装”本身,而在“依赖收集”和“链路验证”。你提前把这两件事做扎实,安装过程就是流水线作业。
最后分享一个小习惯:我每次在内网装完一批软件,会把所有包归档在一个目录里,同时导出一份“安装记录”文档,详细记录每个包的来源、版本、安装时间、依赖关系。后期再出问题,翻这份记录比重新跑一遍ldd、rpm -qa要快得多。这个习惯救过我很多次,建议你从一开始就养成。