Nginx 本身不知道怎么执行 PHP——它是 Web 服务器,不是 PHP 解释器。要让一个 PHP 站点跑起来,需要 Nginx 负责接收 HTTP 请求、把动态请求交给 PHP 解释器处理、拿到结果再返回给浏览器。中间负责承接 PHP 请求的那一层叫 PHP-FPM。加上存储数据的 MySQL 或 MariaDB,就构成了常说的 LNMP 环境。
这套环境的配置点很多,但真正关键的只有几处:PHP-FPM 的进程数量怎么算、Nginx 用什么方式把请求转给它、PHP 文件的匹配规则怎么写才不会被利用、以及出现 502 时从哪查起。这篇文章围绕这几点展开,命令区分 RHEL、CentOS、Rocky 系(dnf 或 yum,软件安装路径与 Debian 系不同,注意 SELinux 影响)和 Debian、Ubuntu 系(apt,PHP-FPM 的套接字路径带版本号)。
PHP-FPM 是什么,为什么不用内置模块
早期常见的做法是让 Apache 通过 mod_php 把 PHP 解释器编译进 Web 服务器进程。这种方式的优点是简单,缺点是致命的:处理静态图片请求时,进程里也带着完整的 PHP 解释器,内存白白被占用,而且 Web 服务器的进程模型直接决定了 PHP 的并发上限。
PHP-FPM(FastCGI Process Manager)是一个独立的进程管理器,通过 FastCGI 协议与 Web 服务器通信。它的价值在于职责分离:
- Web 服务器只处理 HTTP、静态文件、缓存和转发,进程轻量化
- PHP-FPM 独立管理 PHP 进程池,可以独立配置进程数、独立重启,不影响 Web 服务
- PHP 出问题(比如内存溢出被杀)不会连带影响 Nginx 处理静态请求
- 可以为不同站点配置不同的进程池和用户,实现隔离
这种隔离带来的实际收益在内存紧张时特别明显。这也是为什么即使是 Apache 环境,现在的推荐做法也是 mod_proxy_fcgi 加 PHP-FPM,而不是继续用 mod_php。
安装 PHP、PHP-FPM 与常用扩展
先确认系统版本,再安装。
Rocky、RHEL、CentOS 系(较新版本用 dnf,旧版 CentOS 把 dnf 换成 yum):
cat /etc/os-release
sudo dnf upgrade --refresh -y
sudo dnf install -y nginx php php-fpm php-cli php-mysqlnd php-gd php-mbstring \
php-xml php-curl php-zip php-bcmath php-intl php-opcache
sudo systemctl enable --now nginx
sudo systemctl enable --now php-fpm
Debian、Ubuntu 系:
cat /etc/os-release
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx php-fpm php-cli php-mysql php-gd php-mbstring \
php-xml php-curl php-zip php-bcmath php-intl php-opcache
sudo systemctl enable --now nginx
Debian 系安装后 PHP-FPM 通常会自动启动,但服务名带版本号,必须查实际名称:
systemctl list-units --type=service 'php*-fpm.service'
find /run/php -maxdepth 1 -type s -name 'php*-fpm.sock' -print
常用扩展的作用,按需安装:
| 扩展 | 作用 | 不装的后果 |
|---|---|---|
| php-mysqlnd / php-mysql | 数据库连接驱动 | 连不上 MySQL,报驱动不存在 |
| php-gd | 图片处理 | 验证码、缩略图生成失败 |
| php-mbstring | 多字节字符串处理 | 中文截断乱码,很多框架强制要求 |
| php-xml | XML 解析 | RSS、部分接口对接报错 |
| php-curl | HTTP 客户端 | 调用第三方 API 失败 |
| php-zip | 压缩文件处理 | 在线解压、导出压缩包失败 |
| php-bcmath | 高精度数学 | 金额计算类程序可能报警告 |
| php-intl | 国际化支持 | 部分框架要求,缺了会警告 |
| php-opcache | 字节码缓存 | 性能明显下降,建议必装 |
| php-redis | Redis 客户端 | 用 Redis 做缓存或会话时需要 |
装完后检查版本和已启用的模块:
php -v
php -m
php -i | grep -i 'opcache.enable'
如果 php -m 里看不到某个刚装的扩展,通常是 PHP 配置文件没加载它,检查对应 ini 文件是否在扫描目录中。Debian 系可以用 phpenmod 扩展名 启用。
数据库安装两边命令基本一致:
sudo dnf install -y mariadb-server # Rocky、RHEL、CentOS
sudo apt install -y mariadb-server # Debian、Ubuntu
sudo systemctl enable --now mariadb
sudo mariadb-secure-installation # 部分版本命令名为 mysql_secure_installation
安全初始化向导会引导处理匿名用户、远程 root 登录、测试库等。不要图省事设置弱密码,也不要让应用直接使用 root 账号连库。
为站点创建独立账号:
CREATE DATABASE website CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'website_user'@'localhost' IDENTIFIED BY '此处换成随机强密码';
GRANT ALL PRIVILEGES ON website.* TO 'website_user'@'localhost';
FLUSH PRIVILEGES;
localhost 表示只允许本机套接字连接。如果 PHP 与数据库在不同机器,需要把 'website_user'@'应用服务器内网IP' 也授权,并且走内网而非公网。
www.conf:进程管理怎么算进程数
PHP-FPM 的配置文件位置在两系不同:
/etc/php-fpm.d/www.conf # Rocky、RHEL、CentOS
/etc/php/8.x/fpm/pool.d/www.conf # Debian、Ubuntu,版本号以实际为准
先找到自己的实际路径:
sudo find /etc -name 'www.conf' -path '*fpm*' 2>/dev/null
打开后核心内容是进程管理器模式 pm:
| 模式 | 行为 | 适用场景 |
|---|---|---|
static |
始终保持固定数量的子进程 | 内存充足、流量稳定、追求最低抖动 |
dynamic |
在空闲数量上下限之间动态伸缩 | 默认选择,适合大多数场景 |
ondemand |
有请求才创建进程,空闲超时后销毁 | 内存极小、流量很低,或大量几乎无流量的站点共存 |
dynamic 模式需要同时配几个参数:
[www]
user = www-data ; Rocky/RHEL/CentOS 通常为 nginx 或 apache
group = www-data
listen = /run/php/php8.2-fpm.sock ; Debian、Ubuntu 典型路径
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 5s
request_terminate_timeout = 30s
Rocky、RHEL、CentOS 上 listen 常见为 127.0.0.1:9000,这是两系最容易混淆的差异点。不要照抄教程里的路径,务必用 grep 查实际值:
sudo grep -E '^(listen|user|group|pm\.)' /etc/php-fpm.d/www.conf
pm.max_children 的计算方式
这个参数的含义是"最多同时存在多少个 PHP 子进程"。它决定了并发上限,也决定了最坏情况下的内存占用,是 PHP-FPM 调优的核心。
计算公式:
pm.max_children = floor( 可分配给 PHP 的内存 / 单个 PHP 进程的平均常驻内存 )
第一步,确定可用内存。假设一台 4 GB 内存的机器,系统和其他服务约占 1 GB,数据库占 1 GB,留给 PHP-FPM 的是 2 GB,实际可给 PHP 的大约是 1800 MB(留一点余量防止 OOM)。
第二步,测量单个 PHP 进程的实际内存。这是最容易被拍脑袋决定的一步。正确做法是压测后测量:
# 统计所有 php-fpm 子进程的平均常驻内存(单位 KB)
ps -o rss=,cmd= -C php-fpm --no-headers | awk '{ s += $1; n++ } END { if (n>0) printf "avg RSS: %.1f MB, count: %d\n", s/n/1024, n }'
不同程序的差异很大:简单的 WordPress 页面可能在 20 到 40 MB 量级,而带完整框架的 Laravel、Symfony 应用常见 40 到 100 MB,插件多的 CMS 更高。这个数值必须自己测,不要用网上抄来的数字。
第三步,代入公式。若测得平均 40 MB,可用 1800 MB:
1800 / 40 = 45 → pm.max_children = 40(留一点余量)
其他参数的推荐关系:
pm.start_servers:dynamic 模式下启动时的进程数,一般取min_spare + (max_spare - min_spare) / 2pm.min_spare_servers:最少空闲进程,太小会导致流量突增时频繁创建进程产生延迟尖刺pm.max_spare_servers:最多空闲进程,太大则长期占着内存不释放pm.max_requests:单个进程处理多少个请求后自动重启
pm.max_requests 是防内存泄漏的关键设置。PHP 进程长期运行后可能因程序缺陷缓慢泄漏内存,定期重启能释放它。设为 0 表示不重启,生产环境不建议;一般设 500 到 2000。注意请求不重要出不穷才有意义,如果进程数永远到不了这个值,泄露的进程就不会被回收。
调整后的检查动作:
sudo php-fpm -t # Rocky 系常用
sudo php-fpm8.2 -t # Debian 系,二进制名带版本号
sudo systemctl restart php-fpm # 或 php8.2-fpm
Nginx 侧的 fastcgi_pass 配置
Nginx 通过 fastcgi_pass 把请求交给 PHP-FPM,连接方式有两种。
# 方式一:Unix 域套接字
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
# 方式二:TCP 回环地址
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
}
| 对比项 | Unix socket | TCP 127.0.0.1:9000 |
|---|---|---|
| 性能 | 略好,少了 TCP 协议栈开销 | 略低,同机环境下差距通常不明显 |
| 跨机器 | 不支持,仅限本机 | 支持,PHP-FPM 可在另一台服务器 |
| 权限问题 | 依赖套接字文件的属主与权限 | 依赖端口监听,权限问题少 |
| SELinux | 受文件上下文影响 | 受网络连接策略影响 |
| 典型出处 | Debian、Ubuntu 默认 | Rocky、RHEL、CentOS 常见默认 |
选择原则:跟着发行版默认走,改动最小。 同机部署时两者性能差异远小于 PHP 代码本身的影响,不要为了理论上的性能提升去改默认配置,徒增排障难度。只有当 PHP-FPM 部署在独立服务器上时才必须用 TCP 方式。
如果要用 TCP 方式,需要修改 www.conf 的 listen 并保持一致:
listen = 127.0.0.1:9000
listen.allowed_clients = 127.0.0.1
安全提醒:不要把 PHP-FPM 监听在 0.0.0.0:9000 并对公网开放。 9000 端口一旦暴露,攻击者可以直接发送 FastCGI 请求执行任意代码,这是极其危险的暴露面。修改后用下面命令确认没有对外监听:
sudo ss -lntp | grep 9000
location 匹配的安全写法
这一段是整个 PHP 部署中最重要的安全内容,值得逐行讲。
危险写法——很多老教程里能见到:
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $request_filename;
}
这个配置本身在现代版本上不一定直接出事,但它依赖 PHP 侧的一个设置:cgi.fix_pathinfo。当该值为 1 时,对于请求 /uploads/avatar.jpg/x.php,Nginx 因为文件名匹配到 .php$ 会交给 PHP,而 PHP 在找不到 x.php 时会回溯去寻找存在的路径,最终把 avatar.jpg 当作 PHP 文件执行。如果攻击者能上传一个包含 PHP 代码的图片,就能拿到服务器执行权限,这就是所谓的文件解析漏洞。
正确做法是三管齐下:
第一,try_files 确保文件真实存在,不存在直接返回 404。
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
第二,在 php.ini 里关闭 pathinfo 修正。
sudo grep -n 'cgi.fix_pathinfo' /etc/php/*/fpm/php.ini /etc/php.ini 2>/dev/null
确保值为:
cgi.fix_pathinfo=0
第三,上传目录禁止执行 PHP。
location ^~ /uploads/ {
location ~ \.php$ { return 403; }
}
再补一条:隐藏文件一律拒绝访问,避免 .env、.git 之类被下载:
location ~ /\. {
deny all;
}
关于 include fastcgi_params 还是 fastcgi.conf:Debian、Ubuntu 通常提供 snippets/fastcgi-php.conf,其中已经包含了 SCRIPT_FILENAME 的正确定义,直接用 include snippets/fastcgi-php.conf; 更省事;RHEL 系常用 include fastcgi_params; 并自己补一行 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;。两者的区别就是后者默认没有定义 SCRIPT_FILENAME,漏了这一行会出现"空白页"或 No input file specified 错误。
完整可用的 server 配置示例:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.php index.html;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
client_max_body_size 32m;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
include snippets/fastcgi-php.conf; # RHEL 系改用 fastcgi_params 并补 SCRIPT_FILENAME
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_read_timeout 60s;
}
location ~ /\. {
deny all;
}
}
注意 root 指向的是 public 子目录而不是项目根目录。这样即使有人在部署时把 .env 放进了项目根目录,它也不在 Web 可达路径里。
OPcache 参数配置
OPcache 把 PHP 脚本编译后的字节码缓存在共享内存中,避免每个请求都重新解析编译源码。对动态站点来说这是投入产出比最高的一项优化,通常能显著降低 CPU 占用。
配置文件一般在:
/etc/php.d/10-opcache.ini # Rocky、RHEL、CentOS
/etc/php/8.x/fpm/conf.d/10-opcache.ini # Debian、Ubuntu
推荐参数:
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=128
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=2
opcache.save_comments=1
opcache.fast_shutdown=1
参数说明:
| 参数 | 作用 | 建议 |
|---|---|---|
memory_consumption |
缓存字节码的共享内存大小(MB) | 小站点 64 起,普通站点 128,大型项目 256 |
interned_strings_buffer |
字符串驻留内存(MB) | 框架类项目建议 16 或更高 |
max_accelerated_files |
最大缓存文件数 | 要大于项目文件总数,可用选项值需在其中之一 |
validate_timestamps |
是否检查文件修改时间 | 生产环境可设为 0 配合发布时手动重置,性能略好但需自己处理更新 |
revalidate_freq |
检查间隔(秒) | 开启 validate_timestamps 时生效,2 秒是常见折中 |
常见故障:改了代码但页面没变化。 多数情况是 OPcache 缓存未刷新,重启 PHP-FPM 即可:
sudo systemctl restart php-fpm # 或 php8.2-fpm
如果设置了 validate_timestamps=0,必须靠重载 PHP-FPM 或调用 opcache_reset() 来刷新,这也是不推荐新手用这个值的原因。
查看 OPcache 使用情况:
php -r 'print_r(opcache_get_status()["memory_usage"]);'
php -r '$s=opcache_get_status(); echo "hits=".$s["opcache_statistics"]["hits"]." misses=".$s["opcache_statistics"]["misses"]."\n";'
命中率低(比如大量 miss)通常说明 max_accelerated_files 或内存不足,需要加大。
权限与属主
一个清晰且安全的权限模型:
- 网站文件归部署用户所有,Nginx 与 PHP-FPM 只需要读权限
- 只有明确需要写入的目录(上传、缓存、日志、会话)才给写权限
- 永远不要执行
chmod -R 777,这等于允许本机任意用户修改网站代码
Debian、Ubuntu 示例:
sudo adduser --disabled-password --gecos '' deploy
sudo install -d -o deploy -g www-data -m 2755 /var/www/example.com/public
sudo install -d -o deploy -g www-data -m 2770 /var/www/example.com/storage
sudo find /var/www/example.com/public -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/public -type f -exec chmod 644 {} \;
Rocky、RHEL、CentOS 示例(Web 服务组名是 nginx,还要处理 SELinux):
sudo useradd -m -s /bin/bash deploy
sudo install -d -o deploy -g nginx -m 2755 /var/www/example.com/public
sudo install -d -o deploy -g nginx -m 2770 /var/www/example.com/storage
sudo dnf install -y policycoreutils-python-utils
sudo semanage fcontext -a -t httpd_sys_content_t '/var/www/example.com(/.*)?'
sudo semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/example.com/storage(/.*)?'
sudo restorecon -Rv /var/www/example.com
Linux 系统上还有一个细节:套接字文件的属主和权限要允许 Nginx 用户访问。Unix socket 权限不足会直接表现为 502,查看 Nginx 错误日志会看到 Permission denied。
502 Bad Gateway 的排查路径
502 是 PHP 环境下最常见的错误,含义是"Nginx 无法从上游拿到有效响应"。按以下顺序排查,不要跳步。
第一步,PHP-FPM 是否在运行。
systemctl status php-fpm --no-pager || systemctl status 'php*-fpm*' --no-pager
sudo ss -lntp | grep -E '9000|php'
第二步,listen 与 fastcgi_pass 是否一致。 这是最高频的原因,尤其是升级 PHP 版本后套接字路径带了新版本号(php8.1-fpm.sock 变成 php8.2-fpm.sock),Nginx 配置没改。
sudo grep -R '^listen =' /etc/php-fpm.d/ /etc/php/*/fpm/pool.d/ 2>/dev/null
sudo nginx -T | grep fastcgi_pass
nginx -T 会打印所有生效的配置,比逐个文件打开可靠。
第三步,套接字权限。 错误日志里出现 connect() to unix:... failed (13: Permission denied) 就是这个问题,检查 listen.owner、listen.group、listen.mode 与 Nginx 的运行用户是否匹配。
第四步,进程池是否耗尽。 错误日志里出现 connect() to unix:... failed (11: Resource temporarily unavailable),通常是 pm.max_children 太小或请求被慢阻塞。此时要看慢日志和后端数据库。
sudo tail -n 50 /var/log/php-fpm/www-slow.log 2>/dev/null
第五步,SELinux。 RHEL 系上 PHP 或 Nginx 被安全策略拒绝时,不要直接 setenforce 0 了事。先查审计日志确认:
sudo ausearch -m AVC -ts recent
确认是网络连接的拒绝后可按需调整布尔值,而不是整体关闭 SELinux。
第六步,PHP 进程是否因内存被杀。 检查内核日志:
sudo dmesg -T | grep -i -E 'oom|killed process' | tail -n 20
sudo journalctl -k | grep -i 'killed process' | tail -n 20
出现 OOM 说明 pm.max_children 配得过大,或程序确实存在内存泄漏,需要降低进程数并配合 pm.max_requests。
验证与上线的收尾动作
# 语法检查:Nginx 与 PHP-FPM 都要做
sudo nginx -t
sudo php-fpm -t 2>/dev/null || sudo php-fpm8.2 -t
# 平滑重载,而不是直接 restart
sudo systemctl reload nginx
# 本机验证,排除 DNS 与防火墙干扰
curl -H 'Host: example.com' -o /dev/null -sS -w '%{http_code} %{time_total}s\n' http://127.0.0.1/
# 防火墙放行对外端口
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp # Debian、Ubuntu
sudo firewall-cmd --permanent --add-service=http && sudo firewall-cmd --permanent --add-service=https && sudo firewall-cmd --reload # RHEL 系
最后别忘了:测试用的 phpinfo() 文件一定要删除。 它会泄露绝对路径、已加载模块、环境变量和数据库连接相关的配置信息,是常见的敏感信息泄露点。
sudo rm -f /var/www/example.com/public/phpinfo.php
常见问题
页面一片空白没有任何报错,怎么办?
先看 HTTP 状态码。200 空白通常是 PHP 语法错误或致命错误未显示,查看 PHP-FPM 的错误日志;404 检查 root 和 try_files;502 走上面的排查流程。同时确认 display_errors 与 error_log 的设置,生产环境应关闭前台显示、改为写日志。
提示 No input file specified 是什么原因?
RHEL 系最常见,原因是用了 include fastcgi_params; 但没有补 SCRIPT_FILENAME。PHP-FPM 拿不到脚本路径就报这个错。
上传文件失败,但 PHP 配置已经改大了?
Nginx 侧还有 client_max_body_size 限制,默认通常只有 1 MB。Nginx 和 PHP 两边的限制都要调整,取两者较小值才生效。另外还要检查磁盘空间是否足够。
数据库连接很慢,本机却很快,为什么?
一个常见原因是数据库主机填了域名或公网 IP,导致连接要过 DNS 解析或公网往返。同机部署应填 127.0.0.1 或 localhost,跨机则填内网 IP。另外还要检查 MySQL 是否在做反向 DNS 解析,必要时在配置中跳过主机名解析。
用什么命令能看到 PHP 的实际报错?
sudo journalctl -u php-fpm -n 100 --no-pager
sudo tail -n 100 /var/log/nginx/example.com.error.log
sudo tail -n 100 /var/log/php-fpm/error.log 2>/dev/null
不同发行版日志路径不同,可以用 grep -R 'error_log' /etc/php* 找配置里定义的实际位置。
多个站点需要不同 PHP 版本怎么办?
为不同站点启动不同的 PHP-FPM 进程池(各自监听不同的 socket 或端口),在对应站点的 Nginx 配置里写不同的 fastcgi_pass。这比全局切换 PHP 版本安全得多。
总结
- LNMP 的核心是职责分离:Nginx 管 HTTP 与静态资源,PHP-FPM 管 PHP 进程池,两者通过 FastCGI 通信,
listen与fastcgi_pass必须完全一致。 pm.max_children用"可分配内存除以实测单进程常驻内存"计算,单进程内存必须用ps实测而非照抄经验值;同时设置pm.max_requests防止内存缓慢泄漏。- 连接方式跟着发行版默认走:Debian、Ubuntu 多用 Unix socket,Rocky、RHEL、CentOS 常见 TCP 回环,跨机部署才必须改成 TCP,且绝不能监听 0.0.0.0 对外暴露 9000 端口。
- 安全三件套缺一不可:
location ~ \.php$里加try_files $uri =404、php.ini里设cgi.fix_pathinfo=0、上传目录单独禁止执行 PHP,这是防止文件解析漏洞的关键。 - OPcache 必装并保证
max_accelerated_files大于项目文件数;文件属主用部署用户、Web 服务只读,切勿chmod 777;调试用的phpinfo.php用完必须删除。
企业QQ咨询




