先说结论:LNMP这套组合,恐怕是Linux服务器上最经典的Web架构了,没有之一。这篇文章记录了我在实际部署和调优LNMP过程中积累的核心笔记,重点不只是“怎么装上”,而是把Nginx、PHP-FPM、MySQL三者之间的协作原理、关键配置参数、以及各种让新手抓狂的报错根源都梳理清楚。如果你刚接触服务端部署,或者之前一直靠一键脚本/宝塔面板这类工具一手包办,想真正弄明白背后发生了什么,这篇文章会很适合你。
我会从架构设计的角度切入,讲清楚为什么要用Nginx、PHP-FPM究竟解决了什么角色问题,然后再把Nginx虚拟主机配置、FastCGI转发、PHP-FPM进程池调优、MySQL基础优化这些高频操作挨个拆解,最后给出一套手动部署的完整流程,以及我在实际运维中遇到并解决的典型故障案例。全文很长,但每段都是实操里会踩到的点,建议收藏后慢慢看。
1. 整体架构设计与组件选型逻辑
1.1 一次请求走完LNMP的完整链路
在学习配置之前,先把一条HTTP请求在LNMP里的完整路径搞清楚。用户浏览器访问http://example.com/index.php,第一步是DNS解析,把域名换成服务器IP,然后请求到达Nginx监听在80端口的进程上。Nginx拿到请求后发现URI以.php结尾,知道自己处理不了动态逻辑,立即按照配置把请求通过FastCGI协议转交给后端的PHP-FPM进程池。
PHP-FPM接收到请求后,PHP解释器开始执行index.php脚本。假如脚本里有数据库查询语句,比如mysqli_query($conn, "SELECT * FROM users"),PHP会通过socket或TCP协议连接MySQL服务器,发送SQL并等待返回结果集。MySQL把查询结果返回给PHP,PHP再把结果整理成HTML片段,交回给PHP-FPM进程,FPM最后把完整的HTTP响应回传给Nginx,Nginx再还给浏览器。整个链路结束。
这个流程你可以理解成餐厅点餐:Nginx是门口的服务员,静态文件是已经做好的样板菜,直接端上桌;PHP-FPM是后厨团队,接到菜单才开始现场炒菜;MySQL则是食材仓库,后厨需要什么原材料就去仓库取。明白这条链路之后,你就知道排错的大致顺序:页面打不开先看Nginx,PHP没响应查PHP-FPM,数据不对再看MySQL,一层接一层,定位起来非常省时间。
1.2 Nginx与Apache:为什么LNMP默认选择Nginx
很多老教程里的标配是LAMP,也就是把中间那层换成Apache。LNMP和LAMP最大的区别就在这里,这个选择背后是并发模型的不同。
Apache传统上使用进程/线程模型,一个连接基本对应一个进程或线程,面对大量并发请求时,进程频繁创建、销毁、切换上下文,内存占用和CPU消耗都会快速上涨。而Nginx从设计之初就基于事件驱动和异步非阻塞I/O,主进程只负责管理,真正处理请求的是若干worker进程,每个worker通过事件循环同时维护成千上万个连接,单个连接空闲时几乎不占资源,这就是Nginx在高并发静态请求场景下表现更好的根本原因。
我在生产环境实测过,同样的2核4G服务器,单机扛静态页面压测,Nginx轻松跑到几万QPS不崩,Apache配成worker模式要维持相同吞吐量需要的内存和配置成本明显更高。而且Nginx的反向代理能力、Rewrite规则的语法可读性、配置文件的可维护性,对比Apache的.htaccess分散配置模式,都更适合统一管理和多站点部署。
那Apache是不是就完全没用了?也不绝对。有些老项目重度依赖Apache的模块生态,比如某些认证模块或基于.htaccess的规则,迁移成本高。新项目从零搭建,我的建议都是优先Nginx。
1.3 PHP-FPM:PHP和Nginx之间的翻译官
这一步是几乎所有新手理解LNMP的第一个坎:Nginx是Web服务器,它能处理静态文件,但PHP脚本它直接执行不了,因为PHP是一种解释型语言,需要Zend引擎去解析运行。那Nginx怎么把请求交给PHP解释器?
方案是通过FastCGI协议。早期方案里有个PHP-CGI,它是PHP自带的CGI实现,但每次请求都要重新读取配置文件并初始化执行环境,性能极差。PHP-FPM(FastCGI Process Manager)解决的就是这个问题:它启动一个常驻的进程池,提前初始化好PHP执行环境,请求进来直接复用进程处理,处理完继续等待下一个请求,省掉了大量重复初始化开销,同时也提供了进程数控制、平滑重启、慢请求日志等实用功能。
所以你可以把Nginx和PHP-FPM理解成两个互相独立但需要协作的程序。Nginx的fastcgi_pass指令负责把请求“递”给FPM,FPM是个独立的服务,它的进程池监听一个Unix Socket文件或TCP端口。两者的关系就像打电话:Nginx拨号到FPM的监听地址,把请求内容用FastCGI协议口述过去,FPM听懂后执行脚本,再把结果讲回来。PHP从5.4版本开始官方就内置了FPM实现,所以源码编译PHP时启用--enable-fpm,就能得到今天LNMP标配的动态请求处理引擎。
1.4 MySQL与MariaDB怎么选
LNMP里的M,绝大多数发行版默认指向MariaDB,因为它是MySQL的一个分支,由MySQL早期核心团队维护,而且CentOS、Ubuntu等系统源里默认提供的就是它。MySQL和MariaDB在绝大多数操作上完全兼容,SQL语法、连接协议、常用工具几乎一致,很多项目直接在两者间切换都没问题。
选型上我的个人观点是:如果依赖官方发行版的二进制包和长期支持,选MySQL;如果希望跟随系统包管理器获得更无缝的升级体验,选MariaDB。从许可证角度看,MySQL 8.0之后社区版和企业版边界越来越明显,而MariaDB是完全开源自由的,很多对许可证敏感的团队因此优先用它。核心功能层面,MySQL 8.0的窗口函数、CTE(Common Table Expression)这些特性确实好用,如果业务查询依赖这些现代SQL特性,我建议直接上MySQL 8.0。新手学习阶段,两者随便选一个,重点是把连接方式、权限模型、常用存储引擎(InnoDB)的概念学好,这些知识是通用的。
2. 核心配置项逐项拆解:从Nginx到PHP-FPM
2.1 Nginx虚拟主机配置:server块里的关键参数
LNMP下每个网站对应Nginx配置里的一个server块。以下是我常用的虚拟主机模板,每一行都有讲究:
server { listen 80; server_name example.com www.example.com; root /data/www/example.com; index index.php index.html; access_log /var/log/nginx/example.com.access.log; error_log /var/log/nginx/example.com.error.log; location / { try_files $uri $uri/ =404; } location ~ \.php$ { fastcgi_pass unix:/run/php-fpm/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(jpg|jpeg|png|gif|css|js|ico)$ { expires 7d; access_log off; } }server_name是虚拟主机的门牌号,Nginx根据请求头里的Host字段匹配对应的server块,如果配置了多个站点但都写了同一个名字,Nginx默认用第一个匹配到的块,后面的配置再正确也不会生效,这个坑我踩过。root决定站点文件在磁盘的哪个目录,index则是指定当访问/根路径时默认寻找哪个文件,顺序很重要,先写index.php才能让目录请求优先进入PHP入口。
location /里的try_files $uri $uri/ =404;是动态站点的标配。它的意思是:请求example.com/login时,先看磁盘上是否存在对应文件$uri,再看是否存在对应目录$uri/,都找不到就直接返回404。对于WordPress这类需要伪静态的框架,通常还要再接一个rewrite规则把请求转给index.php,让程序内部去路由。
静态文件那个location块里,正则\.(jpg|png|css|js)$会匹配这些后缀的请求,expires 7d告诉浏览器这些资源可以缓存7天,能显著降低服务器压力。这里要提醒:access_log off只是关闭静态资源的访问日志,减少磁盘写入,不要把它用在PHP请求上,否则排查问题时连访问记录都没有。
2.2 FastCGI参数:最容易出错的转发配置
PHP请求能否被正确执行,取决于location ~ \.php$块里的FastCGI配置是否正确。这里最核心的有三样。
第一是fastcgi_pass,后面写PHP-FPM的监听地址。本机通信我强烈推荐用Unix Socket(例如unix:/run/php-fpm/php-fpm.sock),因为它是内核级文件描述符传递,不走TCP协议栈,延迟和开销都更低;只有当你需要把PHP-FPM从Web服务器拆到独立机器时,才改用127.0.0.1:9000这种TCP形式。用Socket时要注意文件权限:如果Nginx的worker进程用户(通常是nginx或www-data)没有该Socket文件的读写权限,你会在日志里看到一样的“connect() failed (13: Permission denied)”。
第二是fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;。这条参数决定了PHP-FPM去磁盘上找哪个脚本来执行。$document_root是当前server块里的root目录,$fastcgi_script_name是请求的脚本路径。比如请求/index.php,最终拼接出的完整路径是/data/www/example.com/index.php。如果这里只写$fastcgi_script_name而缺少root目录,PHP-FPM就会在错误目录下找文件,于是返回“No input file specified”或404。很多迁移过来的项目改了站点目录后忘记同步这条参数,就栽在这里。
第三是include fastcgi_params;,这是引入Nginx自带的公共参数文件(通常位于/etc/nginx/fastcgi_params),里面包含了REQUEST_METHOD、QUERY_STRING等几十个标准CGI环境变量。不要自己一个个手写,漏掉任何一个都可能导致PHP框架功能异常。
顺便说一句超时参数。fastcgi_connect_timeout控制Nginx与FPM建立连接的等待时间,fastcgi_read_timeout控制等待FPM返回结果的时长。如果某个接口执行超过默认的60秒,Nginx会返回504 Gateway Timeout,这时排查方向不是Nginx而是那个具体耗时的PHP脚本或数据库查询。需要延长就调这两个值,单位都是秒。
2.3 PHP-FPM进程池调优:一个公式算出最优进程数
PHP-FPM的配置文件里,[www]是默认进程池,里面几个参数决定了你服务器的并发承载能力。
pm参数有三个模式:static(固定数量)、dynamic(动态伸缩)、ondemand(按需启动)。我建议用表格对比这几个模式:
| 模式 | 行为特点 | 适用场景 |
|---|---|---|
| static | 启动时就固定pm.max_children个进程,数量不变 | 流量平稳、并发较高的生产环境 |
| dynamic | 在设定的上下限内动态调整进程数 | 流量波动明显、无法预估峰值 |
| ondemand | 有请求才创建进程,空闲自动回收 | 低流量、内存紧张的机器 |
新手最容易犯的错误是把pm.max_children设得过大,结果每个PHP进程都占用几十MB内存,高峰期直接把服务器内存打爆,触发OOM Killer。计算进程数有一个朴素公式:max_children = (服务器可用内存 - 系统预留内存) ÷ 单个PHP进程平均内存占用。
具体怎么得到单个PHP进程的内存?用ps -ylC php-fpm --sort:rss查看当前FPM进程的RSS列,取平均值。比如一个典型的WordPress环境里每个PHP-FPM进程稳定在40MB左右,一台2GB内存的服务器,系统本身和其他服务预留500MB,可用的约1500MB,除以40MB得到37.5,那么取32作为初始值就合理。后续配合压测工具(比如ab或wrk)观察内存曲线再微调。pm.max_children不是越大越好,它会直接乘以单个进程内存占用成为实际内存消耗。
日志方面要开慢请求日志。在PHP-FPM池配置里加上:
slowlog = /var/log/php-fpm-slow.log request_slowlog_timeout = 5s这样如果某个PHP请求执行超过5秒,FPM会往慢日志里打印完整的调用栈,定位那些拖垮性能的PHP方法和函数非常方便。
2.4 数据库侧的起步优化:内存与慢日志先搞定
MySQL/MariaDB部署完毕的第一件事不是去调几十个参数,而是改三个最影响体验的地方。
第一个是innodb_buffer_pool_size。这是InnoDB存储引擎用来缓存数据和索引的内存区域,相当于数据库的仓库货架。MySQL官方建议该值设置为服务器物理内存的50%到70%。比如4G内存的机器,设置2G是合理的起点。注意不要在低配机器上无脑调高,留足操作系统和其他程序的内存。默认值通常只有128MB,对这个值不敏感的部署,读写性能差距会非常大。
第二个是慢查询日志。在配置文件[mysqld]段里追加:
slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1long_query_time = 1表示记录执行超过1秒的SQL语句。这里有个细节:这个时间是实际执行时间,不包括等待锁的时间。生成环境开启慢日志后,定期去翻翻,你会发现页面卡顿的元凶往往就藏在这里面——一条没走索引的全表扫描、一个大offset的翻页查询,都比什么框架性能优化来得更直接。
第三个是连接数。默认的max_connections是151,如果你的PHP-FPM同时有几十个进程都在连数据库,高峰期可能出现Too many connections报错。先确认业务并发量,一般调大到200到500之间,不要盲目上万,因为每个连接都有内存开销。配合调整wait_timeout(非交互连接空闲超时),默认28800秒太长,空闲连接会一直占着,建议改成300到600秒让它们尽快被回收。
3. 从零手动部署LNMP环境完整流程
3.1 环境准备:创建专用用户与安装基础依赖
我自己学习部署的过程是:选一台干净的Debian/Ubuntu服务器或虚拟机,2核2G即可,系统用Ubuntu 22.04 LTS。为什么推荐这类系统做学习环境?因为包管理器和社区资料最齐全,踩坑得到的反馈信息也最直接。首先做两件事。
第一是创建独立的运行用户。出于安全考虑,Nginx和PHP-FPM都不应该用root运行。创建nginx用户:
useradd -r -s /sbin/nologin nginx-r表示创建系统用户,-s /sbin/nologin禁止该用户登录,这是常规最小权限做法。PHP-FPM单独创建php-fpm用户,还是同样的命令。
第二是安装编译所需的基础工具。即使你后面打算直接用包管理器,编译依赖也建议提前装好,因为很多PHP扩展在apt里没有预编译版本:
apt update apt install -y build-essential pkg-config autoconf libxml2-dev \ libssl-dev libcurl4-openssl-dev libonig-dev libzip-dev \ libpng-dev libjpeg-dev libfreetype6-dev libsqlite3-dev这些包分别对应PHP编译时的XML、OpenSSL、cURL、mbstring、zip、GD图形库等扩展依赖。漏掉任何一个,configure阶段再回去补装会非常折腾。
3.2 编译安装Nginx及常用configure参数
以编译方式安装并非生产环境唯一选择,但作为学习过程非常推荐,因为每一步依赖和模块都明明白白。到Nginx官网下载稳定版源码包:
wget https://nginx.org/download/nginx-1.24.0.tar.gz tar zxvf nginx-1.24.0.tar.gz cd nginx-1.24.0接下来是configure,这是整个编译安装最关键的一步:
./configure \ --prefix=/usr/local/nginx \ --user=nginx \ --group=nginx \ --with-http_ssl_module \ --with-http_gzip_static_module \ --with-http_stub_status_module--prefix指定安装目录,以后配置文件在/usr/local/nginx/conf,二进制在/usr/local/nginx/sbin。--user和--group指定worker进程的运行身份,务必使用刚才创建的nginx用户。--with-http_ssl_module一定要加,这是HTTPS流量必需的TLS支持模块,漏了之后想补就得重新编译一遍。
编译并安装:
make -j2 && make install编译完成后,Nginx主程序已经就位,但还缺少systemd服务管理。在/etc/systemd/system/nginx.service里写一个Unit文件,设置ExecStart=/usr/local/nginx/sbin/nginx,ExecReload=/usr/local/nginx/sbin/nginx -s reload,然后systemctl daemon-reload && systemctl start nginx。把服务纳入systemd管理后,开机自启、日志查看(journalctl)都顺手了。
3.3 安装PHP并配置PHP-FPM监听方式
PHP同样采用编译安装,可以最大程度控制扩展。去PHP官网下载当前稳定的7.4或8.x版本源码。configure参数里必须包含FPM和业务常用扩展:
./configure \ --prefix=/usr/local/php \ --with-fpm \ --with-mysqli \ --with-pdo-mysql \ --with-zlib \ --with-openssl \ --with-curl \ --enable-mbstring \ --enable-gd \ --enable-zip--with-fpm就是启用PHP-FPM;--with-mysqli和--with-pdo-mysql是PHP连接MySQL的两套接口,建议都启用,因为老代码用mysqli、新代码用PDO,你都要兼容;--with-openssl为的是后续跑HTTPS环境下的加密扩展。
编译安装成功后,PHP的配置文件分两套:php.ini是全局配置,php-fpm.conf和池配置在$prefix/etc/下。从模板拷贝:
cp /usr/local/php/etc/php.ini-development /usr/local/php/etc/php.ini cp /usr/local/php/etc/php-fpm.conf.default /usr/local/php/etc/php-fpm.conf在php-fpm.conf里重点配置listen。我选择Unix Socket:
listen = /run/php-fpm/php-fpm.sock listen.owner = php-fpm listen.group = nginx listen.mode = 0660注意listen.owner和listen.group:Socket文件创建后需要让Nginx的worker进程可读可写,所以把组设为nginx,mode设为0660。前面提到权限问题,就在这页配置上解决。如果改用TCP监听(listen = 127.0.0.1:9000),这三种权限参数就不需要了。
3.4 安装数据库并完成初始化设置
MySQL和MariaDB这里就不重复编译了,用系统源安装最稳妥:
apt install -y mysql-serverUbuntu 22.04安装的是MySQL 8.0。安装完成后用mysqld --initialize完成数据目录初始化(系统包会自动处理),再执行mysql_secure_installation安全脚本,它会引导你设置root密码、移除匿名用户、禁止root远程登录。
这里有个新版本特有的坑:MySQL 8.0默认认证插件是caching_sha2_password,而旧版PHP(比如PHP 5.6、低版本7.x)的mysqlnd驱动不支持这个插件,连接时会报 “The server requested authentication method unknown to the client”。如果你用的PHP版本较旧,需要把应用账号的认证方式改回mysql_native_password,或者升级PHP。这个错误在CentOS 7上的老环境中特别常见,我排查过很多次。
为业务库创建专用账号,遵循最小权限原则:
CREATE DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'web_user'@'127.0.0.1' IDENTIFIED BY 'StrongPass123'; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'web_user'@'127.0.0.1'; FLUSH PRIVILEGES;3.5 创建站点目录并验证LNMP完整闭环
现在三个组件都装好了,把它们串起来。创建站点目录并写入测试文件:
mkdir -p /data/www/example.com chown -R nginx:nginx /data/www/example.com写一个信息页验证PHP:
<?php phpinfo();再写一个连接数据库的测试页:
<?php $conn = new mysqli('127.0.0.1', 'web_user', 'StrongPass123', 'app_db'); if ($conn->connect_error) { die('数据库连接失败: ' . $conn->connect_error); } echo 'LNMP全链路正常:Nginx -> PHP-FPM -> MySQL 均正常'; $conn->close();然后在Nginx虚拟主机配置文件里填入完整的server块,重启Nginx:
nginx -t systemctl reload nginxnginx -t是配置文件语法检查,任何改动后先执行它,确认输出syntax is ok再reload。浏览器访问http://服务器IP/index.php,如果能看到phpinfo页面和数据库连接成功提示,说明这四层已经打通。之后的业务代码,直接放进这个root目录即可。
4. 常见问题与排查技巧实录
4.1 502 Bad Gateway:九成是PHP-FPM没连上
502大概是LNMP环境中最让人印象深刻的报错了。我在实践中总结出的排查顺序是固定的,按这个思路走几乎不会漏。
第一步,确认PHP-FPM进程是否存在:
ps aux | grep php-fpm如果列表为空,说明FPM服务没启动,执行systemctl start php-fpm再看状态。这一步能解决大量刚配置完环境时遇到的502。
第二步,核对监听地址是否一致。在Nginx配置里fastcgi_pass写的是什么,PHP-FPM的listen也必须写什么。一个写socket、一个写127.0.0.1:9000,永远连不上,这种低级错误在反复修改配置时最容易发生。
第三步,检查权限。Nginx日志里出现connect() failed (13: Permission denied) while connecting to upstream,那基本是socket文件权限问题。用ls -l /run/php-fpm/php-fpm.sock查看owner和group,对照前面listen配置的listen.owner、listen.group修正,再chmod 0660 /run/php-fpm/php-fpm.sock。
全部排查完仍然502,最后一招看Nginx错误日志:
tail -50 /var/log/nginx/error.log日志会直接告诉你真正的失败原因,包括超时、连接拒绝、甚至磁盘空间不足。
4.2 PHP文件显示404或被浏览器当作文件下载
请求一个真实存在的PHP文件却返回404,多半是Nginx里缺少PHP的location块,或者路径拼接错误。
先检查虚拟主机配置文件里是否有:
location ~ \.php$ { fastcgi_pass unix:/run/php-fpm/php-fpm.sock; include fastcgi_params; }如果PHP请求走到了静态文件的location里,它会被当成普通文件处理,找不到就404。其次检查SCRIPT_FILENAME参数,确认$document_root的拼接结果对应磁盘上的真实路径。
另一种现象是PHP文件内容被直接下载显示,比如访问返回一串源码文本。这说明Nginx根本没有把请求交给FPM处理,还是因为缺少PHP location或正则写错了。配置里出现的location ~ \.php$里面如果fastcgi_pass误写成了proxy_pass,也会变成下载或转发到错误后端,检查第一眼就先看这一行。
4.3 页面空白与错误信息被吞掉
页面白屏是最让人抓狂的问题,因为PHP默认的display_errors是Off,语法错误、运行时错误都被静默处理了,只会在日志里留一条记录。
处理思路分两步。临时快速定位,先把php.ini里的调试开关打开:
display_errors = On error_reporting = E_ALL log_errors = On改完重载FPM,刷新页面就能看到具体报错。生产环境绝不要长期开启display_errors,这等于把服务器路径、代码结构暴露给任何访问者,看完错误就改回Off。第二步是查PHP-FPM错误日志:
tail -50 /var/log/php-fpm.log它会记录没有输出到页面的所有PHP错误,包括Warning和Notice,配合代码排查可以定位到具体文件和行号。
4.4 性能瓶颈快速定位:Nginx、PHP、MySQL三张日志
页面变慢时,先别急着改代码,把三层日志按时间对齐排查。
Nginx的access log里加上响应耗时字段,在http块中自定义日志格式:
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" rt=$request_time urt=$upstream_response_time';$request_time是完整请求耗时,$upstream_response_time是Nginx等待上游PHP-FPM返回的耗时。如果前者大而后者小,瓶颈在网络或Nginx自身;如果两者都大,瓶颈在PHP执行或数据库。
PHP侧看2.3节启用的php-fpm-slow.log,里面会有超过阈值的函数调用栈。MySQL侧看2.4节的mysql-slow.log,找到高危SQL。三张日志的时间能对上,问题基本就被圈定在某个环节了。这套组合拳,我靠它解决过好几个“页面偶尔卡3秒”的疑难杂症,结果都指向同一条没有索引的SQL。
4.5 上线前必做的安全加固清单
架构能跑只是第一步,安全加固至少要覆盖这几个点,否则裸奔上线就是给自己和用户找麻烦。
第一,PHP侧禁用危险函数。在php.ini的disable_functions里加上system, exec, shell_exec, passthru, proc_open, popen。这对普通业务没影响,但能挡住大量通过命令执行拿权限的WebShell。注意一点:Composer等开发工具需要proc_open,所以生产环境禁用、开发环境保留,分别维护两套php.ini。
第二,MySQL账号最小权限。业务代码绝不使用root连接数据库,应用账号严格按库授权,只给基本的增删改查。前面3.4节创建的web_user给的是Select/Insert/Update/Delete,这对大多数应用已经足够,绝不要随手GRANT ALL PRIVILEGES ON *.*。
第三,防火墙只放必要端口。用ufw或iptables,只允许22(SSH)、80(HTTP)、443(HTTPS)对外,MySQL的3306端口只允许本机或内网特定IP访问。很多用户的数据库被拖库就是因为把3306暴露到了公网。
第四,文件权限收敛。站点root目录下,代码文件设为644、目录设为755,owner使用nginx或独立的部署用户,不要让Web用户对目录有写权限。如果必须开上传目录,单独把那个子目录设为755并确保只写不进,这是Web安全里基础中的基础。
最后再分享一个我个人在多次部署中养成的习惯:每次改动Nginx或PHP配置后,必须执行一次语法检查再reload,Nginx用nginx -t,PHP-FPM用php-fpm -t,习惯只需要几秒钟,却能拦住一大半的“改完配置就宕机”事故。LNMP这套架构看起来简单,但每一层的配置都有它设计的道理,手动完整装过一次之后,你再去看各种面板生成的文件,会发现每行配置都变得非常顺眼。基础打牢了,后面的负载均衡、Redis缓存、消息队列往里加时,你对整个链路会更有掌控感。