PHP网站运行环境怎么搭建?PHP、MySQL和Nginx配置教程

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) / 2
  • pm.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 用完必须删除。