简介:面向信创国产化环境的Nginx离线一键安装包,专为ARM64架构设计,适配银河麒麟V10、openEuler、UOS等主流国产操作系统,面向内网隔离、无法使用yum/apt在线安装的场景,解决Web服务快速部署难题。包内基于Nginx 1.24.0源码,同时预置PCRE、Zlib、OpenSSL依赖库源码包,安装脚本执行时自动编译并完成安装,无需手动干预依赖顺序,大幅降低部署门槛。安装与卸载脚本分离,并提供systemd服务文件,安装后可像常规软件一样用systemctl管理启停;预置nginx.conf,安装目录统一固定在/opt/nginx,便于定位与后期维护。资源共9个文件,以4个gz源码包为核心,配合2个shell脚本、1个conf配置文件、1个service服务文件和1个md说明文档,整体压缩包仅18.99MB,轻量易携带,适合通过U盘或内网中转方式分发。目前已有469人学习使用,适合运维人员、系统集成商及信创项目交付团队参考,也可作为学习离线编译安装Nginx全流程的实操样例。

信创国产化环境Nginx离线一键安装包,我是这样做的

在信创项目里待过的人都知道,最折腾人的往往不是业务逻辑本身,而是环境适配。年初接了一个政务云项目,要求把一套业务系统从原有环境整体迁移到国产化平台,操作系统用的是银河麒麟V10,架构是aarch64,网络环境还是纯内网,连一个外部软件源都访问不了。组里新来的同事第一反应是去官网下载rpm包,结果依赖了一堆包,手动装到一半就卡住了。当时我就决定,干脆做一个Nginx离线一键安装包,把编译好的二进制、依赖、配置文件、service脚本全部打包到一起,装完就能直接用。这篇文章就完整记录一下这个安装包的设计思路和制作过程。

1. 为什么要做离线一键安装包

1.1 信创环境下安装Nginx的三个核心痛点

先说痛点,只有把痛点想清楚了,才知道要做一个什么样的安装包。

第一个痛点是网络隔离。很多信创项目都在政务内网、军工内网或者等保要求严格的数据中心里,物理上就切断了外网连接。你拿着常规的yum install nginx或者apt install nginx命令完全没有用,因为根本没有软件源可以访问。就算你通过临时手段拉了一个源进来,安全审计这一关就过不去。

第二个痛点是国产化操作系统生态差异大。同样是“信创”两个字,背后可能是银河麒麟、统信UOS、中科方德、欧拉、凝思,每个系统的底子还不一样。有的基于CentOS/RHEL体系,有的基于Debian体系,还有的是自己改的包管理方式。哪怕都是基于CentOS体系的系统,系统库版本、glibc版本、架构(x86_64还是aarch64)也可能不一样。所以离线包不能只做一个版本,得考虑多平台兼容。

第三个痛点是运维人员的操作水平参差不齐。很多现场运维人员并不是专职的Nginx管理员,你给他一堆源码让他编译,他容易在configure阶段就卡住;你给他一个rpm包,依赖缺失的时候他也不知道上哪儿找。一键安装包的初衷就是让部署变得足够简单:不管是谁,执行一条命令,等待安装完成,就可以了。

1.2 方案选型:为什么不用yum仓库和容器

如果只是单纯做一个离线rpm包,其实也有路子,比如提前把依赖rpm全部下载下来,放到一个目录里做成local repo。但我实测下来,这条路有几个坑:第一,国产化系统的rpm依赖链经常和标准CentOS不完全一致,同一个包在不同系统上解析出的依赖清单可能不一样;第二,安装rpm包时需要root权限操作系统级目录,卸载时容易留下残留;第三,如果客户后续要求升级Nginx版本,又得重新整理一整套依赖,维护成本很高。

容器方案我也考虑过,比如把Nginx打成Docker镜像直接导过去。但在信创环境里,很多服务器出于安全加固要求没有安装容器服务,内核版本也可能不支持容器所依赖的模块。加上有的客户对镜像供应链有合规要求,容器镜像反而会更难推进。到最后我选了一条最可控、也最通用的路:将编译好的Nginx二进制连同动态依赖库一起打包,做成一个自带安装、卸载、启停脚本的独立目录包。这个方案对系统侵入性小,不污染系统库,清理也干净。

2. 离线安装包的整体设计与目录规划

2.1 安装包的目录结构说明

我设计的安装包整体目录结构大致长这样:

nginx_offline_install/
├── install.sh
├── uninstall.sh
├── README.md
├── conf/
│   ├── nginx.conf.default
│   └── mime.types
└── packages/
    ├── nginx_bin_aarch64_centos/
    │   ├── nginx
    │   └── libs/
    │       ├── libpcre.so.1
    │       └── libssl.so.1.1
    ├── nginx_bin_x86_64_centos/
    └── nginx_bin_aarch64_debian/

每个子目录对应不同的操作系统底座和CPU架构,install.sh脚本在安装时先检测当前系统是哪个分支,再解压对应二进制到部署目录。

这里有一个很关键的设计选择:我为什么不用压缩包(tar.gz)来打包二级目录,而是直接把二进制裸露在packages目录下?原因很简单——安装脚本里可以减少一步解压动作,也方便现场人员直接查看包里的文件。如果以后要更新某个架构的二进制,只要替换对应目录下的文件就行,不用重新打整个tar包。

2.2 为什么坚持用自编译Nginx而不是系统自带版本

很多国产化系统其实自带了一个Nginx,版本通常比较老,或者处于某种不可用的状态。我遇到过一台机器上自带nginx,配置一启动就报错,因为编译时缺模块,运行时报unknown directive。与其依赖系统包,不如自己编一份干净的Nginx放进离线包里。

自编译的时候有几个参数要特别留意,以Nginx 1.24.0为例:

./configure \
  --prefix=/usr/local/nginx \
  --with-http_ssl_module \
  --with-http_realip_module \
  --with-http_gzip_static_module \
  --with-http_stub_status_module \
  --with-stream \
  --with-stream_ssl_module

编译完成后,我建议把编译后的整个目录复制出来,再通过ldd检查依赖。尤其在国产化环境里,如果二进制动态链接了系统里根本没有的.so文件,安装后一启动就报“error while loading shared libraries”。处理这种问题通常有两种方式:一是把缺失的.so文件拷贝到安装包的libs目录下,并在启动脚本里用LD_LIBRARY_PATH指定;二是在编译时尽量静态链接一些关键库,比如OpenSSL、PCRE,但这会增加二进制体积,而且有些场景不允许静态链接。我的折中方案是:默认动态链接,把依赖的.so文件都收集进libs目录,这样既保证了体积可控,也不容易出兼容性问题。

3. 关键实现:安装脚本的完整逻辑

3.1 环境检测与分支选择机制

一键安装包的灵魂在于install.sh脚本的环境检测逻辑。我的脚本先读取/etc/os-release文件判断操作系统类型,再通过uname -m获取CPU架构,然后根据这两个维度选择对应的二进制包。

核心逻辑可以简化成下面这段伪代码:

#!/bin/bash

get_os_type() {
  if grep -qiE "kylin|neokylin|openEuler|CentOS|RedHat" /etc/os-release; then
    echo "centos"
  elif grep -qiE "UOS|Debian|Ubuntu" /etc/os-release; then
    echo "debian"
  else
    echo "unknown"
  fi
}

get_arch_type() {
  case "$(uname -m)" in
    x86_64|amd64) echo "x86_64" ;;
    aarch64|arm64) echo "aarch64" ;;
    *) echo "unknown" ;;
  esac
}

为什么优先读/etc/os-release而不是用rpm或dpkg命令来探测?因为有的信创系统出于安全考虑,可能没装完整包管理工具,但/etc/os-release是所有基于Linux内核的系统都会带的文件。我还遇到过一种情况:系统本身的release文件写的是“Kylin V10”,但底层其实是CentOS 7的用户态,这时候如果单纯按名称归档,容易选错二进制。所以我在检测时还要顺手跑一个命令确认动态链接器类型:

ldd --version | head -n1

这样能拿到glibc的版本范围,做二次校验。

3.2 部署、启动与自启动注册

安装脚本部署Nginx到统一目录/data/apps/nginx下,这个路径是我有意固定的。原因有两个:一是方便统一纳管,不管装哪台机器,Nginx都在同一个位置,后续做监控、日志采集都方便;二是安装脚本可以相对固定地处理升级、回滚等操作。

部署的核心动作是复制文件、创建软链接、注册systemd服务。我把systemd的unit文件直接写在脚本里,用EOF方式生成,避免额外携带文件:

cat > /usr/lib/systemd/system/nginx.service <<EOF
[Unit]
Description=Nginx service (offline package)
After=network.target

[Service]
Type=forking
PIDFile=/data/apps/nginx/logs/nginx.pid
ExecStartPre=/data/apps/nginx/sbin/nginx -t -c /data/apps/nginx/conf/nginx.conf
ExecStart=/data/apps/nginx/sbin/nginx -c /data/apps/nginx/conf/nginx.conf
ExecReload=/data/apps/nginx/sbin/nginx -s reload
ExecStop=/data/apps/nginx/sbin/nginx -s stop
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable nginx.service
systemctl start nginx.service

注意这里Type用的是forking而不是simple,因为Nginx主进程启动后会fork出worker进程,主进程本身变成daemon。如果你用simple模式,systemd会认为主进程退出后服务就结束了,但实际上Nginx还在跑,最后会出现服务状态不准确的问题。我在这上面吃过一次亏,实际表现为systemctl status显示inactive,但业务访问完全正常。

3.3 卸载脚本的边界处理

卸载脚本的核心是clean,但不能上来就rm -rf。我先停服务、再删除systemd的unit文件、最后清理部署目录。这里有个细节:如果部署目录下还有客户自己的配置文件或者日志,直接删除会出大问题。我把卸载脚本分成两种模式:普通卸载只停服务和删二进制,保留conf和logs目录;加--purge参数才彻底清空。

#!/bin/bash

nginx_home="/data/apps/nginx"

systemctl stop nginx.service
systemctl disable nginx.service
rm -f /usr/lib/systemd/system/nginx.service
systemctl daemon-reload

if [ "$1" == "--purge" ]; then
  rm -rf "${nginx_home}"
else
  rm -f "${nginx_home}/sbin/nginx"
  rm -rf "${nginx_home}/modules"
  echo "保留 ${nginx_home}/conf 和 ${nginx_home}/logs"
fi

实际运维中,我见过不少同事卸载时手一抖把正在使用的日志目录也删了,所以这个边界设计真的有价值。

4. 多平台兼容与制作过程实录

4.1 兼容麒麟、统信、欧拉的操作方案

制作安装包之前,我先确认了手头有几类测试环境:银河麒麟V10(aarch64)、统信UOS 1060(x86_64)、openEuler 22.03(x86_64)。这三类系统分别覆盖了“CentOS系+arm”“Debian系+x86”“CentOS系+x86”三种组合,基本就是信创项目里最常见的组合了。

打包时我利用了每种系统的原生编译环境来生成独立二进制。比如需要在aarch64的麒麟机器上执行编译,然后把产物导出;需要在Debian系的UOS机器上执行编译,导出另一份。这个过程不建议跨系统直接复制二进制,因为这样很容易在目标机器上踩到glibc版本不兼容的坑。

一个重要的制作细节是,在打包libs目录时用了一个临时目录把ldd输出里的依赖文件复制进去:

ldd sbin/nginx | grep "=> /" | awk '{print $3}' | xargs -I {} cp -L {} ./libs/

复制时我故意加了大写-L参数,作用是把符号链接指向的实际文件复制进去,否则复制回来的还是符号链接,一旦相对路径断了,运行时就找不到了。

这里还要特别提醒一句:把libs里的.so直接放到安装包里,是为了应急。如果客户对安全合规要求极高,不允许额外携带系统库文件,那就需要在编译时把核心模块都静态编进去。这个决定要提前和客户确认,不要等到现场部署才发现规定不允许。

4.2 二进制包版件管控与升级预留

安装包做出来之后,版本管理也很重要。我不建议在包目录里堆很多无命名规范的二进制,最后分不清哪个对应哪个系统。我的做法是每个架构目录里放一个VERSION文件:

cat > nginx_bin_aarch64_centos/VERSION <<EOF
PACKAGE_NAME=nginx_offline_install
NGINX_VERSION=1.24.0
OS_TYPE=CentOS/RHEL/Kylin/openEuler
ARCH=aarch64
BUILD_TIME=2025-11-20
EOF

这样以后升级Nginx版本时,只需要替换二进制和依赖,然后修改VERSION文件里的版本号即可。安装脚本还可以在安装时把这个信息归档到/data/apps/nginx/VERSION,方便后续排查问题时快速确认线上版本。

5. 常见问题与排查技巧实录

5.1 报错error while loading shared libraries

这个报错几乎每一位离线安装Nginx的运维都会遇到,原因是二进制依赖的某个.so文件在系统里找不到。排查思路很简单:先ldd检查,再对照libs目录看缺了哪一个。

ldd /data/apps/nginx/sbin/nginx

输出里的“not found”项就是罪魁祸首。解决办法是把对应库文件补进libs目录。我遇到过一次比较特殊的情况,缺失的并不是Nginx直接依赖的库,而是它的下游依赖库,ldd输出里没有显示,但运行时它内部又去加载了某个子依赖。这种问题只能靠增加LD_DEBUG=libs环境变量来观察动态链接过程,定位起来稍微费点时间。

5.2 systemd启动失败但手动执行Nginx正常

有同事在现场遇到过这种诡异情况:直接执行/data/apps/nginx/sbin/nginx启动完全正常,但systemctl start nginx.service立刻报失败,journal日志里只有一句“Permission denied”。最后定位到原因:systemd服务默认的ProtectSystem=true会禁止进程写/usr和/boot目录,而配置文件里恰好配置了error_log写到了/usr/local路径下。我当时建议他把日志路径调整到部署目录内部,或者显式给service单元加一行ReadWritePaths,问题就解决了。

5.3 端口被占用导致启动失败

Nginx默认监听80端口,内网机器上很容易被其他服务占掉。解决办法是将配置里的listen改为其他端口,或者在安装前先执行ss -ltnp检查端口占用。有的信创系统审计要求严格,不让轻易改端口范围,那就得在安装包设计阶段预留一个变量,让install.sh可以接收端口参数:

nginx_port=8080
sed -i "s/listen       80;/listen       ${nginx_port};/" /data/apps/nginx/conf/nginx.conf

5.4 版本升级与平滑过渡

Nginx升级其实是对安装包的一个额外考验。我做的安装包预留了升级入口:只要把新版本的二进制放到对应packages目录,再执行install.sh --upgrade,脚本就会先检查新旧版本差异,然后自动备份旧的二进制,再拷贝新的进去,最后执行nginx -t校验配置。没问题之后执行reload,实现平滑升级。

这个流程我在现场验证过,从1.24.0升到1.26.2,在线连接几乎无感知。

6. 实际使用中的几点心得

整套方案从设计到落地,前后迭代了差不多两周时间。最初版本只是把所有文件打个包、写了个简单的安装脚本,但在实际部署中遇到架构选错、依赖缺失、systemd权限拦截等各种问题之后,才逐步完善成现在的样子。

我个人印象最深的一点是:离线安装包最忌讳的不是功能缺少,而是“看起来能用、实际上不可控”。如果安装脚本里全是绝对路径和强依赖,一旦现场环境和预期不符,排查起来会非常痛苦。所以设计安装包时,一定要为“环境探测”和“失败回退”多留几条路。

另外一点,如果你所在的项目不允许在目标机器上保留编译工具链,那建议提前统一一个固定的编译环境,比如直接在CI机里用容器或者虚拟机构建对应发行版的编译环境,把每次编译的依赖记录在案。这样后续做版本迭代和问题回溯,都方便很多。

这套离线安装包目前已经用在了三个信创项目上,累计部署了十几台机器,最长的已经平稳运行了半年多。希望这篇文章能给你一些参考,少踩几个我踩过的坑。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

更多推荐