开头
上个月帮朋友部署一套PHP项目,环境是全新的云服务器,系统装完、Nginx也起来了,结果PHP页面死活就是不对——浏览器访问一个简单的index.php,不是直接变成下载文件,就是返回空白页。后来发现是fastcgi_pass指向的PHP-FPM监听方式和配置里写的不一致,折腾了将近一个小时才定位到。今天把整个LNMP的搭建思路和动静分离的配置整理出来,虽然网上教程一搜一大把,但大多数只告诉你"粘贴这段配置就能跑",不讲为什么这么写、改了会有什么后果、踩坑了怎么排查。这篇文章会用实战的方式完整走一遍从裸机到LNMP跑通、再到动静分离生效的整个过程,重点放在配置背后各组件如何协作的原理上。如果你是刚开始接触LNMP、想在Nginx里部署PHP项目,或者被动静分离这个概念绕晕过,这篇文章应该能帮到你。
1. 先搞清楚LNMP是怎么协作的,再动手搭建
1.1 四个组件各管一摊,别指望Nginx会解析PHP
LNMP这四个字母代表Linux、Nginx、MySQL、PHP。很多人第一次看到这个组合时会以为"Nginx装了就能跑PHP",这是一个非常普遍的误解。Nginx本身就是一个HTTP服务器加反向代理服务器,它天生只会处理静态文件、转发请求、做负载均衡,并不具备解析PHP代码的能力。PHP代码的解析需要PHP-FPM这个独立的进程管理器来完成。
如果把一次完整的用户请求比作去餐厅吃饭,那么Nginx就是门口的迎宾,负责接客、引路、上菜(返回静态资源);PHP-FPM是后厨,负责把菜做出来(解析PHP代码);MySQL是仓库,负责储存食材和账本(存取数据);Linux是整个餐厅的地基。菜品做不做得出来、好不好吃,取决于后厨的水平和食材的新鲜度,但迎宾必须知道"什么样的需求该转给后厨什么窗口",这个"窗口"就是fastcgi_pass指定的地址。
1.2 核心桥梁:FastCGI协议连接Nginx与PHP-FPM
说到fastcgi_pass,必须搞清楚它到底是什么。PHP-FPM在系统中跑起来后,会监听一个地址,这个地址可以是一个Unix套接字文件(比如unix:/run/php-fpm/www.sock),也可以是一个TCP端口(比如127.0.0.1:9000)。Nginx要做的事情,就是把匹配到的PHP请求,按照FastCGI协议的标准格式打包,转发给这个地址,PHP-FPM接到请求后解析执行PHP脚本,再把执行结果原样返回给Nginx,最后由Nginx拼上HTTP响应头返回给浏览器。
这里最容易出问题的地方是:Nginx配置里写的fastcgi_pass地址,必须和PHP-FPM实际监听的地址完全一致。我朋友那次踩的坑就是PHP-FPM默认监听的是sock文件,但Nginx里写的是127.0.0.1:9000,两边对不上,请求转不过去,Nginx就直接返回了502或干脆把PHP文件当静态文件下载了。
1.3 先理解"静态"和"动态"的区别,动静分离才有意义
再来说动静分离。要理解动静分离,先要建立"静态请求"和"动态请求"这对概念。
- 静态请求:请求的是一个实实在在的文件,比如
logo.png、style.css、app.js、.html页面。服务器不需要做任何计算,直接在磁盘上找到这个文件扔回给浏览器就行。 - 动态请求:请求的是一段需要执行的程序,比如
index.php、api/user.php。服务器必须启动PHP解析器来执行这段代码,代码里可能会有数据库查询、业务计算,最后生成一段HTML文本(或者JSON数据)返回给浏览器。
动静分离的核心思想,就是让Nginx充分发挥它的特长——处理静态文件非常快、非常省资源,而让PHP-FPM专心处理动态请求。在配置上,就是通过location匹配规则,把静态文件请求和PHP请求分流到不同的处理路径,静态请求由Nginx直接读取文件返回,动态请求才转发给PHP-FPM。
这个设计在生产环境带来的性能提升非常可观,尤其是高并发场景下,PHP进程不会因为轰炸式的图片、CSS、JS请求而白白消耗宝贵的内存和CPU时间。
2. 环境准备:版本选型和最容易卡住的环境坑
2.1 操作系统与版本组合
本文以CentOS 7.9为例,对应的Nginx版本为1.20.x,PHP选择7.4,MySQL选择5.7(使用yum安装的MariaDB 10.3也可以,两者兼容度足够日常项目使用)。
这个版本组合不是拍脑袋选的。Nginx 1.20系列是2019到2020年间最稳定的维护版本,向下兼容性好,配置语法与最新的1.24、1.25差别不大。PHP 7.4是PHP 8.0发布前最稳的版本,绝大多数旧项目的依赖包都能在上面正常运行;PHP 8.0以上对部分老代码的兼容性问题更多,新项目可以用8.0+,但如果是给现有项目搭环境,7.4更省心。MySQL 5.7是使用量最大的生产版本,它的事务、索引、性能在中等业务量下表现完全够用。
如果你是用的yum安装,建议先执行一次yum -y update,把系统基础软件包版本统一,避免后期出现依赖库版本冲突。我碰到过的最小众坑是:新装系统的curl版本过低,导致后面yum install某些扩展包时出现莫名其妙的依赖解析失败,更新一遍系统后问题直接消失。
2.2 千万记住:Nginx命令找不到,多半不是没装上
在热搜词里我看到"nginx: 未找到命令",这是新手期遇到最多的一个问题。
假如执行nginx -v提示bash: nginx: command not found,第一反应往往是"是不是没装成功"。但实际上80%的情况是装成功了,只是Nginx的可执行文件路径不在当前用户的PATH环境变量里。
yum安装Nginx时,可执行文件默认放在/usr/sbin/nginx,这个目录对普通用户不在PATH中(甚至某些最小化安装的系统中,/usr/sbin也不在PATH里)。所以正确姿势有两种:
# 方式1:使用绝对路径 /usr/sbin/nginx -v # 方式2:找到nginx所在位置后,建立软链接 find / -name "nginx" -type f 2>/dev/null ln -s /usr/sbin/nginx /usr/local/bin/nginx # 方式3:直接切换到root用户 su - root nginx -v从运维习惯来看,方式2最推荐,因为软链接之后无论是nginx -v、nginx -t还是nginx -s reload,普通用户(具备sudo权限即可)都能在任意目录下直接执行,不用每次都敲全路径。
2.3 端口占用检查:80端口没让出来,Nginx起不来
另一个高频报错是Nginx启动失败,错误日志里写着Address already in use。这台机器上如果已经跑着Apache、Tomcat或者其他Web服务,80端口被占用了。
排查命令:
netstat -tlnp | grep :80 # 或 ss -tlnp | grep :80确认占用进程后,要么停掉旧服务,要么修改Nginx默认端口。这里顺带提一下修改端口的方法:编辑/etc/nginx/nginx.conf,找到listen 80 default_server;,把80改成你想要的端口。注意:如果你的server块里写了listen 80;后又写了listen [::]:80;(IPv6监听),两个都要改,否则改完依然会有一个监听在80上。
3. LNMP四件套安装与配置:每一步都讲清楚为什么
3.1 一次安装完成的四个步骤
CentOS 7.9上的安装命令非常集中,依次执行下面四段命令即可:
# 1. 安装Nginx yum install -y nginx # 2. 安装PHP(含常用扩展) yum install -y php php-fpm php-mysqlnd php-gd php-xml php-mbstring php-json php-curl # 3. 安装数据库(CentOS默认源为MariaDB,与MySQL API兼容) yum install -y mariadb-server mariadb # 4. 启动并设为开机自启 systemctl start nginx systemctl start php-fpm systemctl start mariadb systemctl enable nginx systemctl enable php-fpm systemctl enable mariadb几个细节说明:
php-mysqlnd是我们连数据库的驱动,没有它PHP代码里调用mysqli或者PDO连接MySQL会直接报Class 'mysqli' not found。php-fpm安装后默认的监听方式是Unix套接字,监听在/run/php-fpm/www.sock,这个路径后面在Nginx配置里要用到。- 安装完成后建议执行一次
php -v确认版本,再执行php-fpm -v确认PHP-FPM已经就绪。
3.2 Nginx核心配置详解:从全局到server逐层拆解
Nginx配置文件的主结构是嵌套的,主要由以下几层组成:
main(全局配置):worker进程数、日志路径、pid文件 ├── events(事件模型) └── http(HTTP核心模块) ├── 全局配置:日志格式、sendfile、gzip等 ├── server(虚拟主机) │ ├── 监听端口、域名 │ └── location(URL匹配规则)我一般在/etc/nginx/conf.d/下新建一个独立的配置文件,比如lnmp.conf,然后在主配置文件nginx.conf的http块中使用include /etc/nginx/conf.d/*.conf;引入。这是因为nginx.conf文件本身会越来越长,拆分后逻辑清晰,维护方便,也更容易定位问题。
下面是一个带详细注释的核心配置,直接对应LNMP和动静分离场景:
server { listen 80; server_name _; # 使用任意域名或IP访问 root /var/www/html; # 网站根目录 index index.php index.html index.htm; # 静态文件(图片、CSS、JS、字体等) # 命中location后,Nginx直接从root目录读取文件返回 location ~* \.(gif|jpg|jpeg|png|css|js|ico|svg|woff|woff2|ttf)$ { expires 7d; # 客户端缓存7天,减少重复请求 access_log off; # 静态资源访问频繁,不记日志可减少IO压力 } # PHP文件转发给PHP-FPM location ~ \.php$ { root /var/www/html; fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }这段配置里值得重点理解的三个参数:
fastcgi_pass:负责把PHP请求转发到PHP-FPM监听的地址。这里是sock方式,如果你前面PHP-FPM改成了监听9000端口,那这里就要写成fastcgi_pass 127.0.0.1:9000;。SCRIPT_FILENAME:告诉PHP-FPM要执行的文件路径。很多小白在这里栽跟头——如果把$document_root$fastcgi_script_name写成$root$fastcgi_script_name,PHP-FPM就找不到文件,返回404。document_root其实就对应Nginx配置中的root指令的值。include fastcgi_params;:这个文件里定义了大量的FastCGI协议参数,包括REQUEST_METHOD、QUERY_STRING、REQUEST_URI、REMOTE_ADDR等。缺少这个include会导致PHP代码里拿不到$_SERVER里的很多关键信息。
3.3 启动与测试:写一个phpinfo页面验证全链路
配置完成后依次执行:
nginx -t # 检查语法 systemctl reload nginx # 重新加载配置,平滑不中断服务然后在/var/www/html下新建index.php:
<?php phpinfo();浏览器访问http://你的服务器IP/,如果能正常看到PHP的信息页(包含PHP版本、已加载扩展、环境变量等),就说明整个链路已经通了:Nginx收到请求→识别出.php→通过sock转发给PHP-FPM→PHP解析执行→结果返回Nginx→Nginx返回浏览器。
如果出现502,大概率是Nginx连不上PHP-FPM,优先检查fastcgi_pass的地址与sock文件是否存在:
ls -l /run/php-fpm/www.sock如果出现404,重点检查root路径是否写对,以及文件是否存在:
ls -l /var/www/html/index.php如果在浏览器里直接把PHP源码输出成了纯文本,说明location ~ \.php$这个匹配规则没生效,看看是不是配置文件里少写了include fastcgi_params;,或者fastcgi_pass那行被注释掉了。
4. 动静分离实战:让Nginx干它最擅长的活
4.1 动静分离的目录规划与Nginx location匹配优先级
在做动静分离之前,先想清楚一个实际问题:你的项目里,静态文件都放在哪个路径下?
以传统的PHP项目为例,常见结构是这样的:
/var/www/html/ ├── index.php # 动态入口 ├── api/ # 接口目录(动态) │ └── user.php ├── static/ # 静态目录 │ ├── css/ │ ├── js/ │ └── images/ └── uploads/ # 用户上传图片目录动静分离的思路有两种落地方式:
方式一:按扩展名分离。通过正则匹配,命中.css/.js/.png等扩展名的请求直接由Nginx返回文件,其余请求全部交给PHP-FPM。上面的示例配置就是这种方式,优点是配置量少,适用于静态文件分散在多个目录的情况。
方式二:按目录分离。把静态文件统一放在某个前缀目录下(如/static/、/uploads/),用location ^~ + alias来精确控制。
我推荐方式二,因为可以提前在Nginx层面拦截掉大量请求,连PHP-FPM的门都进不去。
4.2 完整配置示例:按目录分离 + 浏览器缓存
server { listen 80; server_name example.com; root /var/www/html; index index.php; # 第一优先级:带^~的精确前缀匹配 # 所有/static/和/uploads/下的请求,都由Nginx直接处理 location ^~ /static/ { alias /var/www/html/static/; expires 30d; # 静态文件客户端缓存30天 access_log off; } location ^~ /uploads/ { alias /var/www/html/uploads/; expires 7d; access_log off; } # 第二优先级:PHP请求转发PHP-FPM location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/run/php-fpm/www.sock; } # 第三优先级:其余所有未匹配到的请求 location / { try_files $uri $uri/ /index.php?$query_string; } }关于location的匹配优先级,很多人背了又忘。这里用最直白的方式做个总结表:
| 写法 | 优先级 | 匹配方式 | 示例 |
|---|---|---|---|
location = / | 最高 | 精确匹配 | 只有访问域名根路径时命中 |
location ^~ /static/ | 较高 | 优先前缀匹配 | 以/static/开头即命中,不再检查正则 |
location ~ \.php$ | 中 | 正则匹配 | 以.php结尾的URL命中 |
location / | 最低 | 通用前缀匹配 | 所有请求兜底 |
对^~要有一个直觉印象:它一旦命中,就不会再往下做正则匹配了。所以如果你有/static/目录,又想拦截其中的/static/xxx.php,那^~会优先命中它,PHP-FPM就收不到这个请求了——PHP文件放静态目录是一个常见的配置错误,这时浏览器会直接下载xxx.php源码,非常危险。
4.3 动静分离的验证方法:看日志和文件大小
配置完毕后,重启Nginx,然后做两个验证:
验证一:确认静态请求没有进PHP-FPM。观察PHP-FPM的请求日志(默认路径为/var/log/php-fpm/www-error.log或通过access.log配置),如果只记录了.php请求,说明动静分离生效了。
验证二:请求头里的Server和响应字节数。用curl -I命令看HTTP响应头:
curl -I http://你的IP/static/css/style.css返回的HTTP头里会包含Content-Length字段,这个值是静态文件的字节数。再去对比一下未分离前的配置,你会发现两种方式的字节数是一致的,但响应时间和Nginx进程占用有巨大差异——在静态文件量大的网站里,动静分离后Nginx的负载能下降50%以上。
个人经验:动静分离的收益在日志里最直观。开启access_log off后,Nginx的访问日志条数会急剧减少(因为静态请求全部不记录),这时你再去tail -f日志,剩下的全是真正的业务请求,排查问题会清爽很多。
4.4 一个必须警惕的坑:上传文件目录也被正则匹配劫持
我实际部署时踩过这样一个坑:项目有一个/uploads/avatar/目录,里面的用户头像都是.jpg文件。一开始我只用"按扩展名"的正则方式做动静分离,所以location ~* \.(jpg|png)$会命中这些头像请求,Nginx直接返回文件,一切正常。
后来同事给/uploads/目录加了一个upload.php上传接口,这时候问题就来了——上传接口的URL是/uploads/upload.php,按正则匹配规则,.php$会优先生效,所以请求被转发给了PHP-FPM,没问题。但上传之后,这些新上传的图片文件存放在/uploads/avatar/下,访问时命中的是location ~* \.(jpg|png)$,也被正确返回了文件。
真正的问题出现在图片防盗链需求上——我后来给/uploads/目录的访问加了一个Referer校验,结果把/uploads/upload.php的动态请求也拦了,上传接口一直报"来源非法"。
这个坑告诉我们的经验是:如果把静态文件和动态脚本混在同一个目录树里,动静分离规则之间的边界要非常清晰。最简单的做法是:静态文件和动态脚本不要放在同一个目录前缀下,比如上传接口独立部署在/api/upload.php,图片统一走/uploads/,这样分离规则就永远不会互相干扰。
5. 上线前的安全检查与性能调优:让LNMP更稳、更快
5.1 配置文件里什么是必须禁掉的
LNMP刚搭完,PHP默认配置存在一些安全隐患,作为实践总结有必要提一下。
打开/etc/php.ini,搜索并修改以下配置项:
; 关闭PHP版本号在前面的响应头中暴露 expose_php = Off ; 关闭危险函数(按需启用) disable_functions = exec,shell_exec,passthru,system,proc_open,show_source ; 控制上传大小,根据项目情况按需调整 upload_max_filesize = 20M post_max_size = 20M修改后重启PHP-FPM:
systemctl restart php-fpm这些配置的目的不是纯堆安全性,更深层的逻辑是:即使将来某个站点被上传了恶意脚本,它能调用的系统函数和能提交的请求大小都被限制在可控范围内。把攻击面先缩小,应用层再配合权限控制,安全性会好很多。
5.2 Nginx性能参数调优(基于事实的保守优化)
Nginx主配置文件/etc/nginx/nginx.conf中两个最重要的参数是worker_processes和worker_connections。
# 设置worker进程数为CPU核心数(也可设为auto) worker_processes auto; # 每个worker最大并发连接数 worker_connections 1024; # 高效传输模型 sendfile on; # 开启gzip压缩,大幅度减少传输体积 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss image/svg+xml; gzip_min_length 1k;参数的简单计算方法是:最大并发连接数 ≈ worker_processes × worker_connections。一台2核4G的云服务器,按auto(通常就是2个worker)+1024连接数,最大并发连接数约2048。但这只是连接数上限,实际能支撑多少取决于PHP脚本执行时间、数据库连接数、带宽等因素,存在木桶效应,别只看这个公式就给系统下结论。
sendfile on很多人不理解。简单说,传统方式下读取静态文件,数据从磁盘读到内核,再从内核复制到用户态程序,再由程序写回内核发给网卡,中间有一到两次多余复制。开启sendfile后,数据在Linux内核里直接完成"磁盘→网络"的传递,效率高很多,对静态文件多的场景提升非常明显。
5.3 日志路径和轮转:排查问题的基础设施
日志是你查一切的底牌。默认情况下:
- Nginx访问日志:
/var/log/nginx/access.log - Nginx错误日志:
/var/log/nginx/error.log - PHP-FPM日志:
/var/log/php-fpm/www-error.log(错误日志)和/var/log/php-fpm/access.log(若开启) - MySQL日志:
/var/log/mariadb/mariadb.log
平时排查502时,我会养成一个习惯:同时打开两个终端,一个tail -f /var/log/nginx/error.log,一个tail -f /var/log/php-fpm/www-error.log,然后在浏览器里再次发起请求,哪边先跳出错误信息,问题就在哪一层。
日志轮转是个容易被忽略的点。Nginx的logrotate配置一般位于/etc/logrotate.d/nginx,默认会按天轮转,保留52份。如果某个项目日志量巨大(比如被扫描器盯上),建议自定义轮转周期和保留份数,避免磁盘被日志占满。具体做法是在配置文件中调整:
/var/log/nginx/*.log { daily missingok rotate 14 # 保留14天 compress delaycompress notifempty create 0640 nginx adm sharedscripts postrotate if [ -f /var/run/nginx.pid ]; then kill -USR1 `cat /var/run/nginx.pid` fi endscript }postrotate里的kill -USR1是让Nginx重新打开日志文件的关键,如果不加这一步,轮转后日志会继续写入已删除的文件句柄,磁盘空间无法释放。
6. 部署完的验证清单与后续扩展
6.1 一张表理清部署后的自检项
LNMP搭建完成后,不要急着部署业务代码,先按下面的清单过一遍:
| 验证项 | 命令或方法 | 预期结果 |
|---|---|---|
| Nginx运行状态 | systemctl status nginx | active (running) |
| PHP-FPM运行状态 | systemctl status php-fpm | active (running) |
| 数据库运行状态 | systemctl status mariadb | active (running) |
| Nginx配置语法 | nginx -t | syntax is ok |
| PHP与数据库连通 | 写一个mysqli_connect测试脚本 | 连接成功 |
| 静态文件直接返回 | curl -I http://IP/static/test.css | 200 OK,Content-Type为text/css |
| 动态请求正常转发 | 访问http://IP/index.php | 正常显示PHP信息页 |
| 端口监听情况 | `ss -tlnp | grep -E ':80 |
其中第五项"PHP与数据库连通"特别重要。很多项目部署后白屏,不是因为Nginx或PHP-FPM没配好,而是PHP代码连不上数据库。验证方式是在网站根目录放一个临时脚本:
<?php $conn = new mysqli('127.0.0.1', 'root', '你的密码', 'test'); if ($conn->connect_error) { die('连接失败: ' . $conn->connect_error); } echo '数据库连接成功';验证后立刻删除这个脚本,千万别留在服务器上——它暴露了数据库地址和账号信息,被扫描器看到是重大安全隐患。
6.2 动静分离后的缓存策略思考
动静分离不只是"把静态交给Nginx"这么简单,还涉及到静态资源的缓存策略。上面配置里写的expires 7d、expires 30d是Nginx给浏览器返回的Cache-Control和Expires响应头,告诉浏览器这个文件在指定时间内可以直接从本地缓存读取,不用再向服务器发起请求。
这带来一个需要留意的副作用:如果哪天你更新了页面里的style.css,但文件名没变,客户端浏览器会继续用本地缓存里的旧文件,新样式不生效。所以现在主流做法是给静态资源文件名加上摘要值(style.abc123.css)或者版本号参数(style.css?v=20250101)。在Nginx层面,expires指令照配,文件名变了URL就变了,缓存自然失效。
在LNMP架构下,做前端资源发布时建议遵循一条原则:静态资源文件名变更走"重命名式"更新,不要覆盖式更新。这样既能享受长缓存带来的性能收益,又不至于让用户看到破旧的页面样式。
6.3 进阶方向:从LNMP到HTTPS与负载均衡
LNMP跑通、动静分离做完,基本功就已经扎实了一大半。下一步可以考虑的扩展方向有三个。
HTTPS证书部署。用certbot申请Let's Encrypt免费证书,在Nginx配置里增加443端口监听和证书路径,把location /中的HTTP请求做301跳转到HTTPS。这一块不难,关键是证书自动续期的定时任务不要漏掉。
PHP-FPM进程数调优。编辑/etc/php-fpm.d/www.conf中的pm.max_children、pm.start_servers、pm.min_spare_servers、pm.max_spare_servers,四个参数需要配合服务器内存大小来设置。每个PHP进程平均占内存约30~50MB,一台2GB内存的机器建议把max_children设为20~30,太高了会导致内存耗尽,太低了并发一上来就502。
多站点配置。在/etc/nginx/conf.d/下新增不同的配置文件,每个server块绑定不同的域名和站点根目录。注意每个server里都要有独立的root和index,否则所有站点都会落到同一个目录里。
我个人在实际部署中的体会是:LNMP这套架构的价值不在于单个组件多强大,而在于组件间的协作方式足够清晰——静态请求和动态请求各走各的通道,互不拖累。把这份协作思路吃透,后面再去理解Nginx的负载均衡、反向代理、缓存策略,基本上就是水到渠成的事。
最后再分享一个小技巧:写完Nginx配置后,改动前一定先备份,改动后一定先执行nginx -t再reload。我被"改了个配置顺手reload结果直接502"这件事教育过太多次,现在每次动配置都遵循这个流程,基本没再出过生产事故。