
做运维和 Web 开发这些年Nginx 是我绕不开的一个老朋友。这个系列写到第三篇前两篇聊了 Nginx 的基础概念和反向代理的基本玩法今天这篇我把压箱底的 LNMP 完整搭建笔记翻出来结合最近一次从零搭建环境的经历把动静分离这块也一并讲透。LNMP 这个组合在中小型项目里出镜率极高Nginx 处理静态请求和反向代理PHP-FPM 处理动态逻辑MySQL 存数据三者各司其职。动静分离听起来高大上说白了就是让 Nginx 直接把图片、CSS、JS 这些静态文件吃掉不要把压力转移到 PHP 和数据库上。这篇文章适合刚接触 LNMP 的同学也适合已经搭过环境但想深入理解 location 匹配、FastCGI 通信和日志定位的同行。全文没有花里胡哨的东西都是我在真实环境里一步步跑通的步骤和踩过的坑。1. LNMP 架构设计与选型思路1.1 为什么是 LNMP 而不是 LAMP聊选型之前先说个最常见的困惑同样都是 Web 服务为什么现在新项目很少选 Apache PHP MySQL也就是 LAMP而是清一色 LNMP最核心的原因在于 Nginx 的并发处理模型。Apache 经典的 prefork 模式是每个连接对应一个进程并发一上来内存和 CPU 开销非常夸张而 Nginx 基于事件驱动一个 worker 进程可以同时处理成千上万个连接。这种轻线程思路在静态文件处理和反向代理场景下优势极其明显打个比方Apache 像一家餐厅给每个客人派一个专属服务员Nginx 则是一个服务员同时照顾几十桌客人效率差距就摆在那。另外Nginx 和 PHP-FPM 的协作方式是请求进来 → Nginx 判断 → 是动态请求就转发给 FastCGI 进程 → PHP-FPM 解析执行 → 返回结果链路清晰可控动静分离天然就是为这种架构设计的。我做过的项目里一台 2 核 4G 的云服务器LNMP 架构扛住日均几万 PV 的 WordPress 站点完全没问题这在同等配置的 Apache 下早就把内存吃满了。所以如果你是从零开始学这套东西我的建议是直接跳过 LAMP从 LNMP 入手是更务实的选择。1.2 组件版本选型与部署方式LNMP 不是一个软件是一组软件的合称Linux Nginx MySQL/MariaDB PHP。版本选型上我的原则是稳定优先不过度追新组件推荐版本说明Nginx1.24.x / 1.26.x stable生产环境不要用 mainline 版本PHP8.1 / 8.2 / 8.3新项目从 8.1 起步7.4 已停止安全维护MySQL8.05.7 已到达 EOL机器配置低可选 MariaDB 10.11LinuxCentOS 7/Rocky Linux/Ubuntu 20.04选你熟悉的发行版即可部署方式我推荐按场景区分。学习或测试环境可以直接用一键脚本或者面板工具能省不少事但注意千万别只依赖面板要理解它帮你做了什么。生产环境建议用发行版官方源安装方便维护和更新容器化场景则用 Docker Compose 编排一套 LNMP 环境用 docker-compose.yml 定义 nginx、php-fpm、mysql 三个服务这是目前很多团队的标准做法。我这次实战记录主要走的是Linux 发行版官方源安装 手动配置的路线这样每一步做了什么、配置文件长什么样都能解释得清清楚楚。2. 核心组件安装与基础配置2.1 Nginx 安装的几种方式与对比Nginx 安装无非三种源码编译、包管理器安装、Docker 容器化。先说源码编译。下载 Nginx 源码包后在./configure阶段可以指定--prefix、--with-http_ssl_module等参数。这里有个容易忽略的点如果你要用 HTTPS必须在编译时加上--with-http_ssl_module否则后面配置 ssl 证书会直接报错。很多新手在网上下了一个编译好的 Nginx发现不支持 SSL就是因为这一步没做。包管理器安装最简单Ubuntu/Debian 用apt install nginxCentOS/Rocky 用yum/dnf install nginx。系统源里的版本可能旧一些但胜在稳定还能通过systemctl直接管理开机自启、服务守护都帮你搞定了。Docker 方式我用得最多的是做本地开发环境nginx:alpine镜像体积小配置通过 volume 挂载到宿主机改完配置docker exec nginx nginx -s reload就能生效。不过用 Docker 跑生产环境日志收集和配置管理会比裸机复杂一些新手建议先在裸机上跑通。给初学者的建议学习阶段老老实实用包管理器安装把精力放在理解配置上不要一上来就折腾编译参数。等你能熟练解释每一行配置含义了再玩源码编译也不迟。2.2 PHP-FPM 安装与基础调优PHP 这边要装的是 php-fpm它才是真正和 Nginx 交互的进程管理器。安装完以后要重点看两个配置文件php.iniPHP 运行时的全局配置upload_max_filesize、post_max_size、memory_limit这几个参数在实战里最常被改比如你要支持用户上传 50MB 的附件光改upload_max_filesize还不够post_max_size必须比上传大小大一点否则大文件 POST 请求会被直接截断。php-fpm.conf和 pool 配置默认情况下有个www.conf里面定义了listen、pm.max_children等关键参数。pm参数是 PHP-FPM 调优的核心。静态模式pm static下max_children开多少就是多少不会动态伸缩好处是性能稳定坏处是流量低峰期也在白白占着内存动态模式pm dynamic下可以设置pm.max_children、pm.start_servers、pm.min_spare_servers、pm.max_spare_servers一整套参数让进程数随负载调整。我常用的一个初始估算方法单进程内存占用约 30-50MB假设机器可用内存 2GB预留 500MB 给系统和其他程序那max_children大约可以设置为 30 左右。这个值不是越大越好开太多进程反而会触发系统 OOM。一个很容易踩的坑修改php.ini或 php-fpm 配置后必须重启 php-fpm 而不是 reload因为有些配置比如环境变量、扩展加载reload 不生效。我习惯用php-fpm -t先检测配置语法再systemctl restart php-fpm宁可多花几秒钟也不要把生产环境搞挂。2.3 MySQL 安装与初始化要点MySQL 安装后第一件事不是建库建表而是先跑安全初始化脚本设置 root 密码、删除匿名用户、禁止 root 远程登录。这一步很多人图省事跳过了后面被扫库勒索才追悔莫及。MySQL 8.0 默认的认证插件是caching_sha2_password老项目里用 PHP 5.x 或特别老的 mysqli 扩展连不上就是因为认证插件不兼容。这种情况要单独建用户并指定mysql_native_password认证方式。如果只是为了学习验证不建议在生产级配置上花太多时间但有两项必须做bind-address默认只监听127.0.0.1如果需要远程连接改成0.0.0.0并配置好防火墙规则只允许特定 IP 访问。开启慢查询日志slow_query_log和long_query_time 2方便后面排查接口慢的问题。我见过太多同学装完 MySQL 就跑来问为什么本地能连服务器上连不上十有八九是改完bind-address忘了检查防火墙端口。记住MySQL 默认端口是 3306安全组和本地防火墙都要放行才能从外部连接。3. Nginx 与 PHP-FPM 的协同与配置文件详解3.1 nginx.conf 整体结构与核心指令Nginx 的配置文件结构可以看成四层main全局、events、http、server、location。初看可能觉得复杂但理解了层级关系就清晰了全局指令影响所有请求http 块里面定义 HTTP 服务的行为server 块对应一个虚拟主机location 块再细化到 URI 的匹配规则。nginx.conf里最常见的几个指令user指定 worker 进程运行用户注意要和 PHP-FPM 的运行用户一致否则会出现权限不足的问题。worker_processes一般设置为auto或者等于 CPU 核心数。设置多了不会提高性能反而增加上下文切换开销。worker_connections单个 worker 能同时保持的连接数配合events块使用默认值是 1024并发高的场景可以调到 4096。include把conf.d或sites-available下的配置文件加载进来多站点管理全靠它。gzip、log_format、upstream这些常见指令我在后面展开讲。配置文件改完一定先nginx -t检查语法再nginx -s reload。这个习惯能避免很多低级错误比如少了一个分号导致整个配置加载失败。3.2 配置一个完整的 server 块fastcgi_pass 与 pathinfo一个能跑 PHP 的 server 块长这样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 unix:/run/php/php8.2-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 7d; access_log off; } }几个关键点fastcgi_pass可以走 TCP127.0.0.1:9000也可以走 Unix Socket/run/php/php8.2-fpm.sock。Unix socket 性能更好但要注意 socket 文件的权限和路径Nginx 的 worker 用户必须对 socket 文件有访问权限。SCRIPT_FILENAME这个参数最关键很多能打开首页但 PHP 文件直接下载的问题都是这里配错了。$document_root$fastcgi_script_name是一种安全可靠的写法。try_files的作用是访问不存在的 URI 时把请求重写到index.php上这是让 Laravel、ThinkPHP 这类框架路由生效的基础。如果你用的是 ThinkPHP 这类需要 PATHINFO 的框架还要在 location 里加fastcgi_split_path_info的配置核心就是告诉 PHP-FPMURL 里哪个部分是脚本名哪个部分是路径参数。没有这一步ThinkPHP 的路由会全部 404。3.3 单机部署多个 Web 项目的三种方案一台机器上要跑多个项目这是非常常见的需求。大概有三种方案不同端口每个 server 块用不同的listen端口比如 8081、8082。适合内部服务或 API客户端访问时要带上端口号不太优雅。不同域名每个 server 块配不同的server_name用域名区分。这是最规范的做法前提是你有域名的 DNS 解析权限。Nginx 会根据请求的 Host 头自动路由到对应的 server 块这就是虚拟主机概念。不同路径前缀一个域名下通过location /projectA和location /projectB来区分。适合临时演示但要注意如果项目本身用了框架路由根路径写死的话这种方式很容易出问题。方案 2 是我最推荐的。一个server_name对应一个项目逻辑清晰、日志分开、证书独立排错的时候一目了然。生产环境里我还习惯把每个项目的配置单独放到/etc/nginx/conf.d/project_name.conf这样即使项目特别多也不会乱。4. 动静分离方案设计与实操4.1 动静分离的判定规则与 location 优先级动静分离本质是通过 location 规则把不同类型的请求分发到不同的处理逻辑静态请求图片、CSS、JS、字体等直接由 Nginx 读取磁盘文件返回不走 PHP。动态请求接口、页面渲染转发给 PHP-FPM 处理。location 匹配规则是这里的核心优先级从高到低精确匹配^~前缀匹配匹配到后不再继续~和~*正则匹配区分大小写 / 不区分大小写/通用前缀匹配我实战里最常用的动静分离写法location ~* \.(gif|jpg|jpeg|png|css|js|ico|woff|woff2|svg|ttf)$ { expires 30d; add_header Cache-Control public, no-transform; access_log off; }这个规则放在 php 匹配之前让静态文件直接不经过 PHP 处理。配合expires设置缓存时间静态资源的重复请求就不需要回源了。需要特别提醒一个坑如果项目里用到了前端构建工具生成的带 hash 的文件名比如app.8f2a1c.js缓存时间可以设长一点但如果是不带 hash 的文件缓存 30 天可能导致更新后用户看到的还是旧版本。这种情况要把资源目录单独处理或者改用Cache-Control: no-cache让浏览器每次重新验证。4.2 静态资源缓存与 Gzip 压缩配置动静分离不只是分流还要配合缓存和压缩才能最大化收益。Gzip 配置在 http 块里gzip on; gzip_comp_level 5; gzip_min_length 1k; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss image/svgxml;压缩级别不是越高越好5-6 是性价比最高的区间再高 CPU 消耗增加明显体积收益却越来越小。我用 1MB 左右的 JS 文件测过gzip_comp_level 6和9的压缩后体积只差不到 2%但 CPU 时间差了将近一倍。缓存方面除了前面 location 里的expires还有两个值得关注的点open_file_cacheNginx 会缓存打开过的文件句柄、文件大小和修改时间对静态文件命中率提升非常明显。配置很简单open_file_cache max1000 inactive20s;如果静态资源放在 CDN那 Nginx 层面的expires主要影响的是 CDN 回源的频率。还有个容易忽略的细节字体文件woff2的 MIME 类型如果没配浏览器会报font/ttf 格式不被允许之类的错误。Nginx 默认的mime.types通常已经覆盖了但自己编译安装时如果不注意可能就会缺记得检查一下。4.3 反向代理场景下的 Header 信息传递动静分离做到后面你会发现实际项目中 Nginx 往往还兼任反向代理的角色最常见的就是把接口转发给后端服务Java、Go、Node。一个典型的location /api/配置location /api/ { proxy_pass http://backend_upstream; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }很多人会问Nginx 转发请求时TCP 层的五元组信息会一起带过去吗答案是默认不会。Nginx 转发时是新建了一条到后端的 TCP 连接源 IP 变成了 Nginx 服务器本身而不是用户真实的 IP。所以需要用X-Forwarded-For这类自定义头部来传递用户的原始信息。后端拿到请求后不要直接取remote_addr要优先解析X-Forwarded-For头。但这里又有一个安全注意点X-Forwarded-For是 HTTP 头客户端可以自己伪造。如果配置里用了$proxy_add_x_forwarded_for那这个值里会包含用户的原始 IP但如果你直接用$http_x_forwarded_for客户端传入的伪造值就可能被后端采信。所以正确做法是进口 Nginx 上用$remote_addr作为第一跳再用$proxy_add_x_forwarded_for在链路上累加后端也至少要取链路上最后一个可信代理添加的 IP不能盲目信任整个头。这个知识点在排查日志里全是内网 IP的诡异问题时非常有用。之前我接过一个项目登录日志里显示所有用户都来自 127.0.0.1就是后端直接取remote_addr导致的。5. 常见问题与排查技巧实录5.1 502 Bad Gateway 的经典排查路径502 是 LNMP 里出现频率最高的错误原因几乎都集中在 Nginx 和 PHP-FPM 的通信上。排查路径我一般按这个顺序看 PHP-FPM 状态systemctl status php-fpm确认进程是不是还活着。看 FastCGI 配置nginx 里fastcgi_pass的地址和 php-fpm 的listen值对不对得上。一个走 unix socket一个走127.0.0.1:9000必挂。看 socket 权限如果走 unix socketNginx 的 worker 用户一般是www-data或nginx必须对 socket 文件有访问权限否则 Nginx 无法连接。看 PHP-FPM 日志默认路径在/var/log/php*-fpm.log里面会明确写出是connection refused还是connect() failed。还有一个很低级但容易犯的错PHP-FPM 服务起来了但监听的是127.0.0.1:9000防火墙或安全组限制了访问导致从 Nginx 机器连不上。内网环境也要记得检查安全组策略。如果你用的是 Docker 部署的 PHP-FPM还有一个额外坑不要用localhost去连要用php-fpm这个服务名或者容器 IP否则连接会被 Docker 网络隔离挡住。5.2 静态资源 404 与权限问题处理静态资源 404 通常是两类原因。一是路径问题。location 里root和alias的差异没搞懂。root会把完整的 URI 拼接到 root 目录后面alias则是把 location 匹配的部分替换成指定路径。location /static/ { alias /var/www/static/; }访问/static/img/a.png时alias会直接映射到/var/www/static/img/a.png。如果写成root /var/www/static/那访问/static/img/a.png时 Nginx 找的是/var/www/static/static/img/a.png。这个坑我帮别人排查过不下十次每次都是配置看着没问题但图片就是 404。二是权限问题。Nginx worker 进程对静态文件所在目录要有读权限对父目录要有执行权限x否则会 403 或 404。最简单的方法是把项目目录属主改成 nginx 运行用户但生产环境建议按最小权限原则来只给需要的目录授权。排查的时候可以先在服务器上用curl http://127.0.0.1/static/xxx.png测试看返回码和响应头基本能定位到是 Nginx 层问题还是后端问题。注意curl时加-I参数只看响应头能省不少流量。5.3 日志分析与安全补丁更新日志是排障的神器也是性能分析的第一手资料。Nginx 的日志路径在配置里由access_log和error_log指定默认情况下access_log/var/log/nginx/access.logerror_log/var/log/nginx/error.logLNMP 的日志还有几个非常规路径PHP-FPM 慢日志slowlog和request_slowlog_timeout配合可以精准抓出哪个 PHP 请求执行时间过长定位慢接口非常有效。MySQL 慢查询日志前面提到过分析 SQL 性能问题全靠它。PHP 错误日志在php.ini里配置error_log路径线上环境建议关闭display_errors把错误写到文件防止敏感信息泄露给用户。安全方面除了配置层面的防护比如限制上传目录执行权限、禁止访问.env等敏感文件还有一件很重要但容易被忽略的事及时关注 Nginx 官方安全公告和你所用发行版的安全更新源。Nginx 官方偶尔会披露 CVE 漏洞社区讨论度很高不用因此恐慌但要认真对待。如果用的是发行版官方源安装的 Nginx可以通过系统包管理工具拿到修复版本如果用的是源码编译的就要自己关注官网的安全公告页面及时下载补丁版本重新编译。这属于平时不觉得出事才后悔的典型场景。最后分享一个我个人的习惯每次改动配置文件之前先备份一份比如nginx.conf.bak-20250101改完验证没问题再删。这个习惯在排查之前还是好的怎么现在不行了这类问题时能帮你快速回滚省下的可不只是半小时。LNMP 这条学习路线从把环境跑通到理解每一个细节靠的就是不断踩坑和复盘希望这篇文章能帮你少走几步弯路。