
做 Nginx 系列实战做到第三篇终于要正面碰一下 LNMP 这套组合了。很多刚入行的同学最早接触的服务器架构就是 LNMPLinux 系统 Nginx 做入口 MySQL 存数据 PHP 处理动态请求。它部署成本低、配置直观、排障链路短特别适合个人项目和中小团队业务。这篇我会把 LNMP 从环境准备到动静分离配置的完整过程拆开来讲包括多站点部署、日志定位、常见 502/404 问题以及一个很多人问过的“Nginx 转发之后客户端 IP 和五元组会不会保留”的问题。文末还会放一份我长期整理的问题速查表都是实际踩过的坑。1. 整体设计与思路拆解1.1 为什么选 LNMP 而不是 LAMP老一代开发者更熟悉 LAMP也就是 Apache MySQL PHP。Apache 的模块化设计很成熟配置也不难但它在高并发场景下比较吃内存每个连接都会对应一个进程或线程。Nginx 则完全不同它采用事件驱动架构一个 master 进程加若干个 worker 进程就能扛住大量并发连接静态文件处理能力尤其突出。对于以 PHP 为主的业务Nginx 负责接收请求、转发动态请求到 PHP-FPM同时直接处理图片、CSS、JS 这些静态资源分工比 Apache 要清晰。PHP-FPM 独立于 Nginx 运行PHP 进程挂掉也不至于让整个入口瘫痪这种进程隔离在实际运维里帮过大忙。如果你要处理的是 Java 微服务体系LNMP 同样可以作为前端的统一入口把请求转发给后端的 Spring Boot 服务。比如若依这类微服务项目部署时经常会把 MySQL、Nginx、后端服务拆成独立单元Nginx 只用来做访问入口、路由转发和静态文件托管架构上反而更干净。1.2 动静分离到底在解决什么问题动静分离的核心思路不复杂动态请求交给 PHP-FPM 处理静态请求由 Nginx 直接从磁盘返回。为什么非要分开因为 PHP-FPM 的处理能力是瓶颈每个 PHP 进程都要启动框架、加载代码、执行逻辑非常耗时。而一张图片、一个 JS 文件根本不需要经过 PHP直接让 Nginx 去读文件返回速度快得多也省下 PHP 进程给真正需要计算的请求。举个例子一个页面里同时包含 index.php 和 30 张图片如果不做动静分离所有请求都会打到 PHP-FPMPHP 进程很快就被占满页面响应会明显变慢。做了动静分离之后只有 index.php 会经过 PHP-FPM图片全部由 Nginx 直接返回顺便还能加上 Cache-Control 让浏览器缓存整体并发能力能提升一大截。动静分离也不只是针对纯 PHP 站点。现在很多 Vue3 项目构建完之后是一堆静态文件再加上一个后端接口服务这种场景和 PHP 站点的处理逻辑其实一模一样静态文件交给 Nginx接口请求转发给后端服务。所以搞清楚这一套后面部署任何前后端分离项目都能用上。2. 环境准备与工具选型2.1 系统版本和软件版本怎么选LNMP 里的 L 可以是 CentOS、Rocky Linux、AlmaLinux、Ubuntu 等主流发行版。CentOS 8 停止维护之后我建议改用 Rocky Linux 或者 AlmaLinux命令和习惯和 CentOS 几乎一致迁移成本很低。如果你更习惯 apt 体系那就用 Ubuntu 22.04 LTS网上资料同样非常丰富遇到问题也容易找到答案。软件版本上我的推荐组合是组件推荐版本备注Nginx1.24 及以上官方主线版或发行版源里的版本都行PHP8.2 或 8.3老项目可以继续用 PHP 7.4新项目建议 8.xMySQL8.0 或 MariaDB 10.11注重兼容性选 MySQL想省内存可以选 MariaDBLinuxRocky 9 / Ubuntu 22.04生产环境优先用 LTS 版本有个容易被忽略的点是 CPU 架构。如果服务器是 aarch64 架构比如国产的 ARM 服务器很多编译好的二进制包不一定能用这时要么找官方提供的 aarch64 版本要么自己准备依赖包做源码编译。我帮朋友在一台 ARM 服务器上部署过 Nginx下载依赖的时候必须专门找 aarch64 的 rpm 包直接用 x86_64 的包会直接报架构不匹配。2.2 包管理器安装和源码编译两条路线普通场景下我强烈推荐用系统包管理器安装。Rocky 系用 dnfUbuntu 系用 apt只需要几条命令就能把 Nginx 装好还自动处理了依赖、开机启动和日志轮转省心太多。# Rocky / AlmaLinux / CentOS 系 sudo dnf install -y nginx php php-fpm php-mysqlnd mysql-server # Ubuntu / Debian 系 sudo apt update sudo apt install -y nginx php-fpm php-mysql mysql-server如果需要指定 Nginx 版本比如装官方仓库里最新的主线版可以通过添加官方 yum 源来解决文件路径通常是 /etc/yum.repos.d/nginx.repo。对大多数业务来说发行版自带的版本已经够用。那什么时候需要源码编译一种情况是内网环境无法访问外网只能离线安装。这时候需要在一台能上网的机器上把 Nginx 及其所有依赖整合成一个目录或者 rpm 包再拷贝到内网服务器上装。另一种情况是需要自定义编译参数比如手动指定 SSL 库、添加特定模块。源码编译前要确认系统里装了 gcc、make、pcre-devel、zlib-devel、openssl-devel不然 ./configure 阶段就会报错。编译完成后用/usr/local/nginx/sbin/nginx -V可以查看编译参数这个命令在排查问题和复现环境时特别好用。如果你不想记那么多命令也可以试试 nginxWebUI、nginx-ui 这类可视化配置工具它们在界面上配置 server、location、证书底层直接生成并校验 Nginx 配置文件。小项目用这些东西确实方便但我的建议是先用命令行把基础概念搞懂再用可视化工具否则出了问题你连配置文件在哪、改了什么都不知道。3. 核心组件搭建过程3.1 Nginx 安装、启动与开机自启在 Rocky 系服务器上安装之后第一件事是启动并设置开机自启sudo systemctl enable --now nginx systemctl status nginx如果系统提示nginx: 未找到命令不用慌大概率是 Nginx 不在当前用户的 PATH 里。用/usr/sbin/nginx -v或者whereis nginx找到实际路径再直接执行即可。源码编译安装的路径一般是/usr/local/nginx/sbin/nginx包管理器安装则在/usr/sbin/nginx。修改端口号也是常见需求比如 Windows Server 上的 Nginx 默认占用 80 端口和 IIS 冲突或者同一个服务器要跑两个站点。操作很简单编辑 nginx.conf找到listen 80;改成listen 8080;然后执行nginx -t检查配置确认无误后systemctl reload nginx或者nginx -s reload。Windows 上则是先nginx -s stop启动nginx.exe重新加载。注意改完端口后防火墙和安全组也要同步放行不然外部访问不到这一步经常被忽略。3.2 Nginx 配置文件结构详解Nginx 配置的层级关系是理解整个配置体系的核心。默认配置文件通常分为几大段main 段管全局参数events 段管连接处理http 段管 HTTP 模块http 段下面可以定义多个 server每个 server 里再定义多个 location。user nginx; worker_processes auto; events { worker_connections 1024; } http { include /etc/nginx/mime.types; include /etc/nginx/conf.d/*.conf; server { listen 80; server_name example.com; location / { root /usr/share/nginx/html; index index.html; } } }include /etc/nginx/conf.d/*.conf;这一行非常建议保留。它意味着你可以在 conf.d 目录下放很多独立的配置文件每个 server 或站点一个文件互不干扰排障时也能快速定位。很多新人把所有 server 全堆在主配置里结果几百行代码改一处错一处维护成本极高。检查配置格式一定要用nginx -t这个命令会告诉你配置文件有没有语法错误。我给自己定了一条规矩任何配置改动不管多小都要先nginx -t再 reload这个习惯帮我避免过好几次把线上服务搞挂的低级失误。3.3 PHP-FPM 安装以及和 Nginx 对接PHP 这边需要安装 php-fpm 和 PHP 连接 MySQL 的扩展。在 Rocky 系系统上一条命令就能搞定sudo dnf install -y php php-fpm php-mysqlnd sudo systemctl enable --now php-fpmUbuntu 系安装 php-fpm 之后PHP-FPM 默认监听的是 unix socket 文件Rocky 系默认监听 127.0.0.1:9000 端口。两者都能用相对而言 unix socket 少了 TCP 封装性能上会好一点点但如果 PHP-FPM 和 Nginx 不在同一台机器上就必须用 TCP 端口。Nginx 里对接 PHP-FPM 的核心配置是这样一段location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }这里的fastcgi_param SCRIPT_FILENAME是关键它告诉 PHP-FPM 要执行哪个文件。如果这个参数配错最常见的现象就是访问 index.php 返回 404 或者直接下载 PHP 文件而不是执行。第一次配置时可以在站点根目录放一个 phpinfo.php 文件测试内容写?php phpinfo(); ?浏览器能打开说明 Nginx 和 PHP-FPM 已经打通。MySQL 安装相对简单装完执行systemctl start mysqld然后运行mysql_secure_installation做安全初始化设置 root 密码、移除匿名用户、禁止 root 远程登录即可。生产环境建议专门建一个业务账号只授权业务数据库别图省事全用 root。4. 动静分离配置实操4.1 用 location 匹配规则做动静分流动静分离的本质是让 Nginx 根据请求的 URI 把请求分到不同处理逻辑里。下面是一个完整的站点配置可以直接参考server { listen 80; server_name example.com; root /var/www/html; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 30d; add_header Cache-Control public, no-transform; } }注意 location 的匹配顺序。Nginx 会先处理精确匹配和最长前缀匹配然后按顺序处理正则匹配。location ~ \.php$和location ~* \.(js|css|...)$都是正则写在哪个先就检查哪个。实际项目中我会把 PHP 的正则放在静态资源正则之前确保动态请求优先被匹配到。try_files $uri $uri/ /index.php?$query_string;这行的意思是如果请求的路径存在就直接返回不存在就交给 index.php 处理。这对 PHP 框架和 Vue 的 history 模式路由都很重要。没有这一行你访问/user/123这种伪静态地址时很容易出现 404。4.2 静态资源缓存策略配合 Vue3 项目的正确打开方式静态资源如果不做缓存每次刷新页面都是一次完整请求图片多的时候体验会很差。上面配置里的 expires 和 Cache-Control 就是在告诉浏览器这些文件 30 天内可以放心用本地缓存。实际部署 Vue3 项目时构建出来的 dist 目录里JS 和 CSS 文件通常都带哈希指纹文件名形如app.8f3c2b.js。这种文件名每次有内容变化都会变所以非常适合长缓存。我会单独配一个 location 给 assets 目录设置一年缓存location /assets/ { alias /var/www/html/dist/assets/; expires 1y; add_header Cache-Control public, immutable; }但 index.html 绝对不能长缓存。因为它的内容引用的 JS/CSS 文件名在不断变化如果浏览器缓存了旧的 index.html即使你发布了新版本用户也只会请求到旧的文件名导致页面还是旧版本。处理方式是给 index.html 单独设置不缓存或者协商缓存location /index.html { add_header Cache-Control no-cache, no-store, must-revalidate; }这是所有前后端分离项目发布后“页面不更新”的经典原因不用怀疑九成是这个缓存策略没配对。4.3 动静分离涉及的几个细节坑第一个坑是 root 和 alias 搞混。root 会把 location 的匹配路径拼接到 root 目录后面alias 则是用 alias 目录替换 location 路径。比如 location /assets/ 配 root /var/www/html实际读取的是 /var/www/html/assets/ 下的文件如果配 alias /var/www/html/dist/实际读取的就是 /var/www/html/dist/ 下的文件。少写一个斜杠都可能直接 404。第二个坑是大文件的上传和响应超时。PHP 默认的 upload_max_filesize 是 2M很多项目传大附件时报错找不到原因。同时Nginx 转发大请求时如果后端处理超过 60 秒会出现 504 Gateway Timeout。此时要调大fastcgi_read_timeout或者proxy_read_timeout还要同步调整 PHP 的 max_execution_time两头缺一不可。第三个坑是 gzip。静态文件加上 gzip 压缩后体积能小 70% 以上。在 http 段里开启gzip on;并设置gzip_types包含 text/css、application/javascript、application/json 等类型。但要注意已经压缩过的图片格式如 JPEG、PNG、WebP 不要再做 gzip收益低还浪费 CPU。5. 多站点、日志与转发细节5.1 一台机器上部署多个 Web 项目部署多个项目有几种常见方式。一是用不同域名区分每个域名对应一个 server 块互不影响二是用同一个域名的不同路径区分在一个 server 块里写多个 location三是用不同端口区分比如 81 端口跑项目 A82 端口跑项目 B。域名方式最干净配置文件结构是server { listen 80; server_name a.example.com; root /var/www/project_a; index index.php index.html; } server { listen 80; server_name b.example.com; root /var/www/project_b; index index.php index.html; }子路径方式需要注意路径和根目录的对应关系。比如/app1/这个路径想访问 /var/www/app1正确写法是location /app1/ { alias /var/www/app1/; index index.html; }如果写成 root /var/www/app1/Nginx 实际找的路径会变成 /var/www/app1/app1/很容易踩坑。端口方式最简单每个 server 块里 listen 不同的端口即可Windows Server 上修改 Nginx 端口号也是同样的原理找到对应 server 块的 listen 改成新端口就行。微服务场景下多项目的另一个含义是多个后端服务。Nginx 可以定义多个 upstream 转发到不同后端然后各个接口路径对应不同 location。这种方式适合把接口请求和静态页面统一托管在同一个入口下。5.2 Nginx 日志路径与排障思路日志是排障的第一工具我建议在配置里显式指定日志路径别用默认值瞎猜。常见的配置log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent; server { access_log /var/log/nginx/example_access.log main; error_log /var/log/nginx/example_error.log warn; }访问日志能看到每个请求的来源 IP、路径、状态码、响应大小、User-Agent 等信息。报错时第一件事就是打开 error_log 看最后几行tail -f /var/log/nginx/error.log502 Bad Gateway 时error.log 里通常会有类似connect() failed (111: Connection refused)的记录说明 Nginx 连不上 PHP-FPM。这时候去看 PHP-FPM 的状态有可能是进程没启动、监听端口写错、或者 unix socket 文件权限不对。504 超时的问题前文已经提过调大 timeout 相关参数即可。日志文件越来越大也会拖累服务器建议配置 logrotate 做日志轮转。包管理器安装的 Nginx 一般自带轮转配置源码编译的话需要自己写。我踩过最尴尬的一次坑是日志目录权限不对Nginx worker 进程没有写权限结果访问日志完全不更新排查了大半天才找到原因。5.3 用 Docker 部署 Nginx 的常用方式现在不少团队把 Nginx 容器化。Docker 方式最大的好处是环境和宿主机隔离想换版本、换模块只需要替换镜像。最基础的运行命令docker run -d \ --name nginx \ -p 80:80 \ -v /var/www:/usr/share/nginx/html:ro \ -v /etc/nginx/conf.d:/etc/nginx/conf.d:ro \ nginx:1.26把宿主机上的站点目录和配置目录挂载进容器更新代码时不需要动容器。需要修改配置后 reload执行docker exec nginx nginx -t再docker exec nginx nginx -s reload。多个服务一起部署时更推荐用 docker compose。一个典型的 LNMP 环境会包含 nginx、php-fpm、mysql 三个服务compose 里定义好镜像、端口映射、数据卷一条docker compose up -d就能全部拉起来。这样维护起来很舒服但前提是你对 Nginx 配置本身有足够的理解否则容器化只会让排查链路更长。5.4 Nginx 转发之后客户端 IP 和五元组信息会保留吗这个问题是我在技术群里被追问过很多次的经典问题展开讲一下。IP 头部的五元组指的是源 IP、源端口、目标 IP、目标端口、协议类型。当一个客户端直接访问 NginxNginx 拿到的是真实用户和 Nginx 之间连接的五元组信息。但当 Nginx 把请求转发给后端 PHP-FPM 或后端 API 服务时它会建立一条新的 TCP 连接这时候后端看到的源 IP 是 Nginx 所在机器的 IP源端口也是 Nginx 随机分配的端口用户原始的源 IP 和源端口已经不存在了。所以直接回答默认情况下Nginx 转发不会把客户端原始五元组完整带给后端。但如果你只需要保留客户端的 IP完全可以通过 HTTP 头来实现。Nginx 提供了两个很常用的变量$remote_addr就是直连 Nginx 的客户端 IP$proxy_add_x_forwarded_for会在原有 X-Forwarded-For 基础上追加当前直连 IP。实际配置可以这样写server { # HTTP 接口转发场景 proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; # PHP-FPM 场景则在 fastcgi_params 里增加 fastcgi_param HTTP_X_REAL_IP $remote_addr; fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for; }后端程序读取 X-Real-IP 或 X-Forwarded-For 就能拿到真实用户 IP这对于登录日志、风控、访问统计都很有用。需要注意两层转发的情况比如 Nginx 前面还有负载均衡设备那么 $remote_addr 拿到的是上一层的 IP而不是最终用户 IP此时要基于请求头里的 X-Forwarded-For 继续往后追加后端的取值逻辑也得对应调整。6. 常见问题速查与避坑6.1 常见问题速查表现象可能原因解决方案nginx: 未找到命令不在 PATH 中用/usr/sbin/nginx或/usr/local/nginx/sbin/nginx执行访问站点返回 403目录索引不存在或权限不足检查 index 配置、目录可读权限、SELinux 状态访问 index.php 返回 404SCRIPT_FILENAME 配置错误检查 fastcgi_param SCRIPT_FILENAME 是否拼接了正确的 document_rootPHP 文件被直接下载Nginx 没有匹配到 PHP location确认 location ~ \.php$ 块存在且 fastcgi_pass 正确502 Bad GatewayPHP-FPM 未启动或监听地址不对检查 php-fpm 状态核对 fastcgi_pass 的端口或 socket504 Gateway Timeout后端处理超时调大 fastcgi_read_timeout / proxy_read_timeout同步调 PHP 的 max_execution_time静态资源 404root/alias 使用错误或路径少斜杠确认 root 拼接规则alias 时注意 location 路径写全端口被占用80 端口被其他服务占用修改 Nginx listen 端口或先停掉占用端口的服务配置改了没生效没有 reload先nginx -t再nginx -s reload访问日志不更新日志目录权限不够检查 error.log给 nginx 用户日志目录写权限6.2 我踩过的坑和独家建议第一个一定要记住的教训每次改 Nginx 配置先备份再改动。我习惯在改之前执行cp nginx.conf nginx.conf.bak.$(date %F_%T)即使改坏了也能快速回滚。线上出问题时冷静做回滚比现场改配置靠谱得多。第二个是 SELinux 的问题。Rocky 系默认开启 SELinux经常出现明明 Nginx 配置和权限都对但转发请求就是失败的情况。排查时执行getenforce看状态如果是 enforcing并且确实需要 Nginx 访问外部的后端服务可以执行setsebool -P httpd_can_network_connect 1这句话的意思是允许 Nginx 进程发起网络连接。这个坑对新手非常不友好因为它不会体现在 Nginx 错误日志里或者只会给出很模糊的 permission denied。第三个是关于 PHP-FPM 的 unix socket 权限。如果你使用/run/php-fpm/www.sock这种方式Nginx worker 进程必须有权限访问这个 socket 文件。常见做法是调整 www.conf 里的 listen.owner 和 listen.group将其设置为 nginx 用户和组或者把 nginx 用户加进 php-fpm 的组。权限不对的典型表现就是 502但很多人第一时间不会想到 socket 权限。第四个建议是动静态分离配置完成后用 curl 做一轮基础验证。比如curl -I http://你的域名/static/test.js看返回头的 Cache-Control 是否生效curl -I http://你的域名/index.php看是否正常返回 200 而不是 404。这种验证方式比浏览器强在哪浏览器会有缓存干扰判断curl 每次都是干净请求能看到完整的响应头。最后分享一个我自己的习惯每搭一个环境我会先把域名、端口、站点目录、日志路径、PHP 版本这些信息记在一张表里再开始写配置。看起来是笨办法但排查问题时能省一半时间。动静分离的配置本身并不难难的是把缓存策略、日志、权限、超时这些边角料都处理干净。LNMP 这套组合我已经搭过很多次每次回过来看最大的体会是先把最小闭环跑通再去叠加缓存、多站点和容器化一步一步来反而最快。