1. 为什么需要在一台服务器上跑多个PHP版本
一台服务器只跑一个PHP版本,这在几年前还算常规操作。但现在的情况变了——你可能同时维护着三四个项目,有的跑的是老框架,只兼容PHP 7.2;有的用了新特性,必须上PHP 8.1;还有的客户指定要PHP 7.4,说升级会炸。这时候如果每个项目配一台服务器,成本直接起飞,运维也累得够呛。
Nginx根据域名解析多个PHP版本,说白了就是让同一个Nginx监听80或443端口,根据请求的域名不同,把PHP请求转发给不同的PHP-FPM进程池。比如old.example.com走PHP 7.2,new.example.com走PHP 8.1,api.example.com走PHP 7.4。用户访问的时候完全无感知,但后端各跑各的,互不干扰。
这个方案解决的核心问题是资源复用与版本隔离。一台机器、一个Nginx、多个PHP-FPM实例,每个实例监听不同的端口或Socket,Nginx在server块里根据server_name匹配域名,然后把.php请求通过fastcgi_pass指向对应的PHP-FPM。整个过程逻辑清晰,维护成本低,特别适合中小型项目、测试环境、多客户站群这类场景。
适合谁来参考?如果你手头有多个PHP项目需要共存,又不想折腾容器编排,或者你对Docker还不够熟悉,想先用传统方式快速搞定,那这套方案就是给你准备的。下面我会从整体设计、核心细节、实操过程到问题排查,一步步拆开讲。
2. 整体架构设计与方案选型思路
2.1 为什么选Nginx+多PHP-FPM而不是其他方案
多PHP版本共存,常见的有三种路子:一是Docker容器,每个项目一个容器,隔离性最好,但资源开销大,网络配置也麻烦;二是多台服务器,简单粗暴但费钱;三是单Nginx+多PHP-FPM,也就是本文要讲的方案。
我选第三种,理由很直接:轻量、可控、调试方便。PHP-FPM本身就是独立进程池,不同版本监听不同端口,天然隔离。Nginx作为反向代理层,只负责根据域名转发,不关心后端跑的是哪个版本。这种架构下,你不需要额外学容器编排,不需要处理容器网络,日志也都在同一台机器上,排查问题快得多。
当然,这个方案也有局限。如果项目之间需要完全的文件系统隔离、或者依赖不同的系统库,那还是得上容器。但对于大多数“同一台机器跑多个PHP项目”的需求,Nginx+多PHP-FPM已经够用了。
2.2 域名解析与PHP-FPM的映射逻辑
核心逻辑其实就一句话:Nginx的server块根据server_name匹配域名,然后在location ~ .php$里把请求转发给对应的PHP-FPM监听地址。
举个例子,假设你有两个域名:
site-a.example.com需要PHP 7.4site-b.example.com需要PHP 8.1
那Nginx配置里会有两个server块,分别写server_name site-a.example.com;和server_name site-b.example.com;。在各自的location里,fastcgi_pass分别指向127.0.0.1:9074和127.0.0.1:9081。PHP-FPM那边,7.4版本的池监听9074,8.1版本的池监听9081。请求进来,Nginx一看域名,就知道该往哪个端口送。
这里的关键是端口规划。我习惯用90xx系列,比如9072对应PHP 7.2,9074对应7.4,9080对应8.0,9081对应8.1。这样一眼就能看出端口和版本的对应关系,不容易搞混。
2.3 方案的优势与潜在坑点
优势前面说了,轻量、直观、易调试。但坑点也得提前说清楚:
- PHP-FPM配置文件路径不同:不同版本的PHP-FPM,配置文件可能在
/etc/php/7.4/fpm/pool.d/或/etc/php-fpm.d/,取决于系统。你得先确认清楚。 - Socket vs TCP:用Unix Socket性能稍好,但跨版本管理时TCP端口更直观。我一般用TCP,方便用
netstat或ss快速确认哪个版本在跑。 - Nginx的fastcgi_params路径:不同系统下
fastcgi_params文件位置可能不同,有的在/etc/nginx/,有的在/usr/local/nginx/conf/。配置时要用绝对路径,避免找不到。 - 权限问题:PHP-FPM的运行用户必须能读取项目文件,否则会报
Access denied。每个池的user和group要设置正确。
这些坑我在后面会详细展开,先有个印象就行。
3. 核心细节解析与实操要点
3.1 PHP-FPM多实例的安装与端口规划
假设你用的是常见的Linux发行版,安装多个PHP版本通常有两种方式:一是用系统包管理器(如apt或yum)安装不同版本的PHP包;二是自己编译。我推荐用包管理器,省事。
以Debian/Ubuntu为例,安装PHP 7.4和PHP 8.1:
sudo apt install php7.4-fpm php8.1-fpm安装完成后,系统会自动创建对应的FPM服务,比如php7.4-fpm和php8.1-fpm。默认情况下,它们可能都监听9000端口,这就会冲突。所以第一步是改端口。
PHP 7.4的池配置通常在/etc/php/7.4/fpm/pool.d/www.conf,找到listen = /run/php/php7.4-fpm.sock或listen = 127.0.0.1:9000,改成listen = 127.0.0.1:9074。同理,PHP 8.1改成listen = 127.0.0.1:9081。
改完后重启服务:
sudo systemctl restart php7.4-fpm sudo systemctl restart php8.1-fpm然后用ss -tlnp | grep php确认端口监听情况。你应该能看到9074和9081分别被不同的PHP-FPM进程占用。
注意:有些系统上PHP-FPM的池配置文件可能不在
pool.d目录下,而是直接在php-fpm.conf里。用php-fpm7.4 -tt可以测试配置文件语法,并显示实际加载的配置文件路径。
3.2 Nginx server块的域名匹配与fastcgi_pass配置
Nginx这边,核心是写好server块。假设项目A的代码在/var/www/site-a,项目B在/var/www/site-b。配置大概长这样:
server { listen 80; server_name site-a.example.com; root /var/www/site-a; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9074; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } } server { listen 80; server_name site-b.example.com; root /var/www/site-b; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9081; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }关键点:
server_name必须和实际访问的域名一致,否则Nginx会走默认server块。fastcgi_pass指向对应的PHP-FPM端口。SCRIPT_FILENAME必须正确设置,否则PHP-FPM找不到文件。include fastcgi_params引入通用参数,不同系统路径可能不同,用nginx -V查看编译时的默认路径。
配置完成后,nginx -t测试语法,然后systemctl reload nginx重载。
3.3 如何验证每个域名走的是正确的PHP版本
验证方法很简单:在每个项目的根目录放一个info.php,内容为<?php phpinfo(); ?>。然后分别访问http://site-a.example.com/info.php和http://site-b.example.com/info.php,看输出的PHP版本是否对应。
如果版本不对,排查顺序:
- 确认Nginx配置里
fastcgi_pass端口是否正确。 - 确认PHP-FPM实际监听的端口是否和配置一致。
- 确认访问的域名是否匹配到了正确的server块(可以看Nginx访问日志)。
- 确认没有其他server块拦截了请求(比如默认server)。
实操心得:我习惯在
info.php里加一行echo $_SERVER['SERVER_NAME'];,这样能同时确认域名和PHP版本,一举两得。
4. 完整实操过程与核心环节实现
4.1 环境准备与PHP多版本安装
假设你有一台干净的Ubuntu 22.04服务器,已经装好了Nginx。现在要加PHP 7.4和PHP 8.1。
首先添加第三方PPA(因为Ubuntu 22.04默认源里没有PHP 7.4):
sudo apt install software-properties-common sudo add-apt-repository ppa:ondrej/php sudo apt update然后安装:
sudo apt install php7.4-fpm php7.4-mysql php7.4-curl php7.4-mbstring sudo apt install php8.1-fpm php8.1-mysql php8.1-curl php8.1-mbstring安装完成后,检查服务状态:
systemctl status php7.4-fpm systemctl status php8.1-fpm如果两个服务都active,说明安装成功。接下来改端口。
4.2 修改PHP-FPM池配置并启动多实例
编辑/etc/php/7.4/fpm/pool.d/www.conf,找到listen行,改成:
listen = 127.0.0.1:9074同样,编辑/etc/php/8.1/fpm/pool.d/www.conf:
listen = 127.0.0.1:9081顺便检查一下user和group,确保和Nginx运行用户一致(通常是www-data):
user = www-data group = www-data改完后重启:
sudo systemctl restart php7.4-fpm sudo systemctl restart php8.1-fpm用ss -tlnp | grep -E '9074|9081'确认端口已监听。
4.3 Nginx配置拆分与域名绑定
现在配置Nginx。我习惯把每个域名的配置拆成独立文件,放在/etc/nginx/sites-available/下,然后软链到sites-enabled/。
创建site-a.conf:
server { listen 80; server_name site-a.example.com; root /var/www/site-a; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9074; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }创建site-b.conf:
server { listen 80; server_name site-b.example.com; root /var/www/site-b; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_pass 127.0.0.1:9081; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }然后:
sudo ln -s /etc/nginx/sites-available/site-a.conf /etc/nginx/sites-enabled/ sudo ln -s /etc/nginx/sites-available/site-b.conf /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx4.4 测试与验证:用phpinfo确认版本隔离
在/var/www/site-a和/var/www/site-b下分别创建info.php:
<?php phpinfo(); echo 'SERVER_NAME: ' . $_SERVER['SERVER_NAME'];然后本地用curl测试(假设域名已解析到本机):
curl -H "Host: site-a.example.com" http://127.0.0.1/info.php | grep -i "PHP Version" curl -H "Host: site-b.example.com" http://127.0.0.1/info.php | grep -i "PHP Version"你应该看到两个不同的版本号。如果相同,说明配置没生效,检查Nginx是否重载、PHP-FPM是否重启、端口是否对应。
注意:如果服务器有防火墙,确保80端口开放。另外,如果用了CDN或反向代理,
SERVER_NAME可能不是预期值,需要看X-Forwarded-Host之类的头。
5. 常见问题与排查技巧实录
5.1 访问PHP文件返回502 Bad Gateway
这是最常见的问题。502通常意味着Nginx无法连接到PHP-FPM。排查步骤:
- 检查PHP-FPM是否在运行:
systemctl status php7.4-fpm。 - 检查端口是否监听:
ss -tlnp | grep 9074。 - 检查Nginx配置里的
fastcgi_pass端口是否和PHP-FPM监听一致。 - 检查SELinux或防火墙是否拦截了本地回环通信(一般不会,但值得一看)。
如果PHP-FPM没起来,看日志:journalctl -u php7.4-fpm -n 50。常见原因是配置文件语法错误,用php-fpm7.4 -tt测试。
5.2 访问PHP文件返回404 Not Found
404说明Nginx找不到文件。检查:
root路径是否正确。SCRIPT_FILENAME是否拼写正确。- 文件权限是否允许Nginx用户读取。
我遇到过一种情况:root路径末尾多了斜杠,导致$document_root$fastcgi_script_name拼出来是/var/www/site-a//info.php,虽然Linux通常能处理双斜杠,但某些PHP-FPM版本会报404。去掉多余斜杠就好了。
5.3 PHP版本不对,所有域名都走同一个版本
这说明Nginx没有正确匹配server_name,所有请求都落到了默认server块。排查:
- 确认
server_name拼写和访问域名完全一致。 - 确认没有其他server块用了
default_server且匹配了所有域名。 - 看Nginx访问日志,确认请求落到了哪个server块。
实操心得:我习惯在Nginx配置里加
access_log /var/log/nginx/site-a.access.log;,每个域名独立日志,排查时一目了然。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 502 Bad Gateway | PHP-FPM未启动或端口不对 | 检查服务状态和端口监听 |
| 404 Not Found | root路径或SCRIPT_FILENAME错误 | 检查Nginx配置和文件权限 |
| 版本不对 | server_name未匹配或fastcgi_pass错误 | 看访问日志和PHP-FPM端口 |
| 权限拒绝 | PHP-FPM用户无法读取文件 | 检查文件权限和池的user/group |
| 配置重载失败 | Nginx语法错误 | nginx -t测试 |
6. 进阶技巧与长期维护建议
6.1 用Unix Socket替代TCP端口
TCP端口直观,但Unix Socket性能稍好,且不占用端口号。配置方法:PHP-FPM池里listen = /run/php/php7.4-fpm.sock,Nginx里fastcgi_pass unix:/run/php/php7.4-fpm.sock;。注意Socket文件的权限,确保Nginx用户能读写。
我一般在小规模部署时用TCP,大规模或性能敏感时用Socket。两者切换成本很低,改一行配置的事。
6.2 日志分离与监控
每个域名独立access_log和error_log,方便排查。PHP-FPM也有自己的慢日志,在池配置里开启:
slowlog = /var/log/php7.4-fpm-slow.log request_slowlog_timeout = 5s这样能抓到执行超过5秒的脚本,对性能优化很有帮助。
6.3 版本升级与回滚策略
当某个项目需要从PHP 7.4升级到8.1时,不要直接改现有配置。正确做法是:先装好PHP 8.1的FPM,配好新端口,然后新建一个server块,用新域名或子域名测试。测试通过后,再把原域名的fastcgi_pass指向新端口,重载Nginx。这样回滚只需要改回端口,风险极低。
我在实际维护中,会保留旧版本的FPM实例至少一个月,确认没问题再停掉。这期间如果新版本出问题,改一行配置就能切回去。
6.4 资源限制与进程池调优
每个PHP-FPM池都可以独立设置pm.max_children、pm.start_servers等参数。如果一台机器上跑多个池,总进程数不能超过内存承受能力。我的经验是:先估算每个池的最大并发,然后加总,确保总内存占用不超过物理内存的70%。比如每个PHP-FPM进程占50MB,8个池各开10个进程,就是4000MB,4GB内存的机器就吃紧了。
可以用pm = ondemand模式,按需启动进程,节省内存。但ondemand模式下,第一个请求会有启动延迟,适合访问量不高的场景。
最后分享一个小技巧:如果你不确定某个域名走的是哪个PHP版本,可以在Nginx配置里加一个自定义header,比如
add_header X-PHP-Version "7.4";,这样用浏览器开发者工具看响应头就能确认,不用每次都翻phpinfo。这个技巧在排查多版本混跑时特别管用。