1. 为什么在 RedHat 9 上还要折腾源码编译
在 RedHat 9 上装 Nginx,绝大多数人的第一反应是dnf install nginx,一条命令搞定,省心省力。但我在实际运维和交付环境里,遇到需要源码编译的场景远比想象中多。最典型的情况是:系统自带的软件源里 Nginx 版本偏旧,而业务侧需要用到某个较新版本才引入的模块特性;或者需要把某些第三方模块(比如一些自定义的鉴权、日志、限流模块)静态编译进去,包管理器装的二进制根本没法加;再或者交付环境对软件来源有严格要求,必须从源码构建、可控可审计。
源码编译这件事,说难不难,说简单也真不简单。难的不是敲那几行./configure && make && make install,而是编译过程中各种依赖缺失、路径配置、权限归属、后续升级维护的坑。我见过太多人第一次编译完能跑起来,结果过两个月想加个模块,发现当初编译参数没记,只能推倒重来;也见过编译出来的 Nginx 日志目录权限不对,跑一段时间磁盘写满或者权限报错。所以这篇内容我想把 RedHat 9 这个特定环境下源码装 Nginx 的完整链路讲透,包括每一步为什么这么做、参数怎么选、装完之后怎么验证、以及那些文档里不会写的经验。
适合谁看?如果你是有一定 Linux 基础、需要在 RedHat 9(或同源的兼容发行版)上从源码构建 Nginx 的运维、后端或者交付工程师,这篇能直接抄作业;如果你只是想快速跑个 Nginx 做测试,那包管理器确实更合适,但读完你也能明白源码编译到底在做什么。下面我按实际操作的顺序展开,中间穿插大量踩坑经验。
2. 编译前的环境盘点与依赖补齐
2.1 先搞清楚 RedHat 9 的软件源现状
RedHat 9 默认的软件仓库策略和 CentOS 时代有区别,很多在 CentOS 7 上yum install就能拿到的开发包,在 RedHat 9 上需要先确认仓库是否启用。编译 Nginx 需要的核心依赖其实不多,但每一个都不能少。我一般先做一次系统更新和仓库检查:
dnf update -y dnf repolistrepolist的输出能让你确认BaseOS和AppStream这两个基础仓库是否都在。RedHat 9 把很多开发工具拆到了AppStream里,如果这个仓库没启用,后面装gcc、pcre-devel会直接报找不到包。这一步看起来废话,但我遇到过交付环境为了“精简”把 AppStream 禁用了,结果卡在装依赖上半天。
2.2 编译工具链和库依赖的完整清单
Nginx 源码编译依赖分两类:一类是编译工具本身,一类是功能库。工具链方面需要gcc、make,如果要用到 C++ 相关或者某些模块,还得有gcc-c++。功能库方面,最核心的是三样:
- PCRE:正则表达式库,Nginx 的
location正则匹配、rewrite规则全靠它,没有它编译会直接失败。 - zlib:gzip 压缩模块依赖,做静态资源服务基本必开。
- OpenSSL:如果要配置 HTTPS,这个必须有。
对应的开发包在 RedHat 9 里叫pcre2-devel、zlib-devel、openssl-devel。注意这里有个容易踩的坑:RedHat 9 默认的 PCRE 已经是 PCRE2 了,而老版本 Nginx 对 PCRE2 的支持是从 1.21.5 之后才比较完善。如果你编译的是很老的 Nginx 版本,可能会遇到找不到pcre.h的问题,因为 PCRE2 的头文件路径和 PCRE1 不一样。我的建议是直接用较新的稳定版 Nginx,省去这层兼容麻烦。
一次性把依赖装齐:
dnf install -y gcc make gcc-c++ pcre2-devel zlib-devel openssl-devel装完可以用rpm -q逐个确认,别嫌麻烦,我习惯装完立刻验证,避免编译到一半才发现某个包其实没装上。
2.3 一个常被忽略的前置动作:创建专用运行用户
很多人编译完直接./nginx用 root 跑起来,测试没问题,上线就出事。Nginx 的 worker 进程应该用非特权用户运行,这是基本的安全实践。RedHat 9 上包管理器安装会自动创建nginx用户,但源码编译不会。所以编译前先手动建好:
groupadd -r nginx useradd -r -g nginx -s /sbin/nologin -M nginx-r表示系统用户,-s /sbin/nologin禁止登录,-M不创建家目录。这几个参数不是随便写的:系统用户 UID 范围小、权限受限;禁止 shell 登录防止被拿来做跳板;不建家目录是因为 Nginx 运行根本不需要。后面./configure时用--user=nginx --group=nginx指定,编译出来的配置默认就带上了,省得装完再改。
提示:如果你打算复用已有的用户,务必确认该用户没有登录 shell 且权限足够低,别图省事用
nobody,某些系统上nobody的权限边界很模糊。
3. 源码获取与 configure 参数的门道
3.1 下载源码包与校验
源码从 Nginx 官方站点获取,选稳定版(stable)分支。下载后我强烈建议做一次完整性校验,尤其是生产环境:
wget https://nginx.org/download/nginx-1.24.0.tar.gz wget https://nginx.org/download/nginx-1.24.0.tar.gz.asc拿到.asc签名文件后可以用gpg --verify校验,确认包没被篡改。这一步在内部交付环境里经常被省略,但只要出过一次下载损坏或者中间被替换的事,你就会养成校验的习惯。校验通过后解压:
tar -zxvf nginx-1.24.0.tar.gz cd nginx-1.24.03.2 configure 参数到底该怎么选
./configure是整个编译过程里最需要动脑的一步,参数选错了后面全是返工。我先给一个我常用的、兼顾功能和精简的配置:
./configure \ --prefix=/usr/local/nginx \ --sbin-path=/usr/local/nginx/sbin/nginx \ --conf-path=/usr/local/nginx/conf/nginx.conf \ --pid-path=/usr/local/nginx/logs/nginx.pid \ --lock-path=/usr/local/nginx/logs/nginx.lock \ --error-log-path=/usr/local/nginx/logs/error.log \ --http-log-path=/usr/local/nginx/logs/access.log \ --user=nginx \ --group=nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_gzip_static_module \ --with-http_stub_status_module \ --with-pcre \ --with-stream \ --with-stream_ssl_module逐个解释关键参数背后的逻辑。--prefix决定安装根目录,我习惯统一放/usr/local/nginx,好处是所有文件集中,卸载时直接删目录,升级时也好备份。--sbin-path、--conf-path这些显式指定路径,是为了让目录结构清晰可控,避免默认值散落在系统各处。--pid-path和--lock-path放 logs 目录下,方便排查进程状态。
模块方面,--with-http_ssl_module和--with-http_v2_module是现在做 Web 服务的基本盘,HTTP/2 对静态资源加载提升明显。--with-http_realip_module在走反向代理或负载均衡时用来获取真实客户端 IP,不做这层配置日志里全是代理的 IP,排查问题很痛苦。--with-http_gzip_static_module支持预压缩的.gz文件直接返回,比运行时压缩省 CPU。--with-http_stub_status_module提供基础的状态监控页,轻量够用。--with-stream是四层代理模块,做 TCP/UDP 转发时用得上,即使暂时不用,编译进去也不占多少体积,省得以后再加。
这里有个经验:不要盲目--with-*全开。有些模块依赖额外的库,全开会引入不必要的依赖和攻击面。按实际需求选,把编译参数记到一个文件里(比如build-args.txt),以后升级或者加模块时直接复用,这是我最想强调的习惯。
3.3 configure 阶段的报错怎么读
./configure跑完会输出一个汇总,列出哪些模块启用、哪些没启用、依赖检查结果。如果中途报错,最常见的是the HTTP rewrite module requires the PCRE library,说明 PCRE 开发包没装或者路径没找到。RedHat 9 上如果装的是pcre2-devel但 Nginx 版本较老,可能仍然找不到,这时候要么升级 Nginx 版本,要么手动指定--with-pcre=../pcre-源码目录让它用自带的 PCRE 源码编译。
还有一种报错是SSL modules require the OpenSSL library,检查openssl-devel是否真的装上了,有时候仓库里同名包版本不匹配也会导致检测失败。我的做法是看config.log,里面记录了每个检测项的详细过程,比终端上那几行报错信息有用得多。
4. 编译安装与目录权限的收尾处理
4.1 make 与 make install 的实操细节
./configure成功后,执行:
make -j$(nproc) make install-j$(nproc)用满所有 CPU 核心并行编译,能显著缩短时间。但要注意,如果编译机器内存很小(比如 1G 的测试机),并行度过高可能触发 OOM,这时候把-j后面的数字调小,比如-j2。我遇到过在低配虚拟机上-j8直接卡死的情况,排查半天才发现是内存不够。
make install会把文件按 configure 时定的路径铺开。装完先别急着启动,做两件事:检查目录结构和处理权限。
4.2 目录归属与权限的坑
源码编译出来的目录,默认属主是执行编译的那个用户(通常是 root)。Nginx 的 worker 进程以nginx用户运行,它需要对某些目录有写权限,主要是存放临时文件、缓存、PID 和日志的目录。如果权限不对,启动时可能报Permission denied,或者运行一段时间后日志写不进去。
我的处理方式是:
chown -R root:root /usr/local/nginx chown -R nginx:nginx /usr/local/nginx/logs chown -R nginx:nginx /usr/local/nginx/client_body_temp chown -R nginx:nginx /usr/local/nginx/proxy_temp chown -R nginx:nginx /usr/local/nginx/fastcgi_temp chown -R nginx:nginx /usr/local/nginx/uwsgi_temp chown -R nginx:nginx /usr/local/nginx/scgi_temp主目录和二进制归 root,防止被普通用户篡改;日志和临时目录归 nginx 用户,保证 worker 能写。client_body_temp这些临时目录在首次启动时才会创建,如果启动前不存在,可以先手动建好再赋权,避免首次请求上传大文件时报错。
注意:不要图省事把整个
/usr/local/nginx都chown给 nginx 用户,配置文件归普通运行用户所有是安全隐患,一旦该用户被攻破,配置可被随意篡改。
4.3 验证安装结果
启动前先用-t检查配置语法:
/usr/local/nginx/sbin/nginx -t输出syntax is ok和test is successful才算过。然后启动:
/usr/local/nginx/sbin/nginx用ps -ef | grep nginx确认 master 和 worker 进程都在,worker 进程的属主应该是nginx。再curl -I http://127.0.0.1看返回头,正常应该看到Server: nginx/1.24.0。最后看error.log有没有异常输出,这一步很多人跳过,结果带着隐患上线。
5. 装完之后必须补的几件事
5.1 配置 systemd 服务管理
源码编译不会自动生成 systemd unit,手动敲命令启停不现实。自己写一个/etc/systemd/system/nginx.service:
[Unit] Description=nginx - high performance web server After=network.target remote-fs.target nss-lookup.target [Service] Type=forking PIDFile=/usr/local/nginx/logs/nginx.pid ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf ExecReload=/usr/local/nginx/sbin/nginx -s reload ExecStop=/usr/local/nginx/sbin/nginx -s quit PrivateTmp=true [Install] WantedBy=multi-user.targetType=forking是因为 Nginx 默认以守护进程方式运行,master 进程 fork 后父进程退出,systemd 需要靠PIDFile追踪。ExecStartPre里加-t检查配置,配置有语法错误时服务直接起不来,而不是带着错误配置跑起来,这个细节能省很多事。ExecStop用-s quit而不是-s stop,quit是优雅退出,会等当前请求处理完,stop是快速终止,生产环境优先优雅。
写完systemctl daemon-reload,然后systemctl enable --now nginx设置开机自启并立即启动。
5.2 日志轮转不能忘
源码装的 Nginx 日志默认无限增长,跑久了磁盘会被撑爆。用系统的logrotate来管,新建/etc/logrotate.d/nginx:
/usr/local/nginx/logs/*.log { daily rotate 14 missingok notifempty compress delaycompress sharedscripts postrotate /usr/local/nginx/sbin/nginx -s reopen endscript }关键是postrotate里的-s reopen,它让 Nginx 重新打开日志文件句柄,这样轮转后的新日志才能被正确写入。没有这一步,轮转后 Nginx 还在往被重命名的旧文件里写,新文件一直是空的,这是日志轮转最经典的坑。
5.3 升级与加模块的可持续方案
源码编译最大的痛点是后续维护。我的做法是:把当初的./configure参数完整保存到/usr/local/nginx/build-args.txt,升级时下载新版本源码,用同样的参数重新 configure、make,然后不要直接 make install 覆盖,而是先make install到一个临时目录,对比新旧文件,确认无误后再替换二进制、平滑 reload。加模块也是同理,在原有参数基础上追加--add-module=模块路径,重新编译。
平滑升级可以用信号机制:把新二进制放到临时位置,给旧 master 进程发USR2信号,它会启动新的 master 并用新二进制,旧 worker 继续服务存量请求,处理完再发WINCH和QUIT逐步退出。这套流程稍微复杂,但能做到零停机,值得单独花时间练一遍。
6. 那些文档里不会写的踩坑记录
6.1 编译时找不到 OpenSSL 的诡异情况
有一次在 RedHat 9 上编译,openssl-devel明明装了,./configure还是报找不到 OpenSSL。查了半天发现系统里同时存在多个 OpenSSL 版本,头文件路径和库路径不一致。解决办法是在 configure 时显式指定:
--with-openssl=/path/to/openssl-source让 Nginx 用自带的 OpenSSL 源码编译,绕开系统路径混乱的问题。这个坑的教训是:依赖检测失败不一定是没装,可能是装了多个版本互相干扰,config.log里能看到它到底在哪些路径找过。
6.2 防火墙和 SELinux 的隐形拦截
RedHat 9 默认启用 firewalld 和 SELinux。Nginx 起来后本机curl正常,外部访问不通,八成是防火墙没放行 80/443:
firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --reloadSELinux 更隐蔽,如果 Nginx 要监听非标准端口或者访问某些目录,即使权限看着都对也会被拦。排查时先看ausearch -m avc -ts recent有没有拒绝记录,确认是 SELinux 问题再针对性调整策略,别上来就setenforce 0关掉,那是把安全机制整个废了。
6.3 编译参数丢失导致的返工
这个坑我踩过不止一次。早期编译时随手敲参数,没记录,后来要加个模块,完全想不起来当初开了哪些选项,只能凭记忆重来,结果新编译的版本行为和线上不一致,排查了很久。从那以后我强制自己把参数写进文件,并且把nginx -V的输出也存档,-V会打印完整的编译参数和版本信息,是最可靠的“编译指纹”。每次升级前先nginx -V对比,确保参数一致。
6.4 临时目录缺失引发的上传失败
Nginx 处理客户端上传的大请求体时,会先写到client_body_temp目录。源码编译后这个目录可能不存在,小请求没事,一旦上传大文件就报 500。手动创建并赋权就能解决,但更稳妥的做法是在部署脚本里把几个 temp 目录统一建好,别等出问题再补。
7. 我个人在实际操作中的体会
源码编译 Nginx 这件事,技术门槛其实不高,真正拉开差距的是对细节的把控和后续可维护性的设计。我现在的习惯是:编译前把依赖清单、用户创建、参数选择一次性规划好,编译后立刻补 systemd、日志轮转、参数存档这三件事,把“能跑”变成“能长期稳定跑”。RedHat 9 这个环境相对规范,仓库和工具链都比较新,只要注意 PCRE2 的兼容性和 SELinux 的拦截,整体流程是顺畅的。
最后分享一个小技巧:如果你不确定某个模块要不要编译进去,先查它的依赖和体积,依赖轻、体积小的模块(比如stub_status、realip)建议直接编进去,省得以后为了一个小功能重新编译整个 Nginx;依赖重或者有安全考量的模块,按需再加。编译参数存档加上nginx -V指纹,这两样东西能让你在半年后接手自己当初的构建时,不至于一脸茫然。