☰
PHP8.5配置多站点虚拟主机怎么操作
2026/10/2 10:29:19 网站建设 项目流程

前言

最典型的症状是这样的:服务器上配了a.com和b.com两个站点,浏览器访问b.com,返回的却是a.com的首页;或者访问任何一个域名,都落到同一个目录里。还有一种更隐蔽的症状——页面能打开,但一执行$_SERVER['HTTP_HOST']相关的逻辑就串站,A 站的用户会话跑到了 B 站。

这类问题几乎都不是 PHP 本身的问题,而是 Web 服务器(Web Server)的虚拟主机(Virtual Host)匹配规则没配好。虚拟主机分两类:基于名称的(name-based,靠Host请求头区分)和基于 IP 的(IP-based)。现在绝大多数场景用的都是基于名称的,而基于名称的虚拟主机有一条铁律——请求头里的Host决定一切,匹配不上就走"默认主机"。

本文以 PHP 8.5 为例(PHP 8.5 已于 2025 年 11 月发布,是当前的稳定版本),分别给出 Nginx 与 Apache 的多站点配置,并重点讲 PHP-FPM(FastCGI Process Manager)进程池的隔离,最后列出几个最容易踩的坑。文中所有配置都按"能直接抄进生产"的标准写。

一、基于名称的虚拟主机是怎么匹配的

无论 Nginx 还是 Apache,匹配流程都是同一个逻辑:


  1. 取出 HTTP 请求头里的Host(不含端口号);

  2. 在所有虚拟主机里找server_name/ServerName与它相等的;

  3. 找不到就找server_name里带通配符(如*.example.com)能匹配上的;

  4. 还找不到,就用第一个被加载的虚拟主机作为默认主机。


第 4 条就是所有"串站"问题的根因。很多人以为"没匹配到会报 404",实际不会,它会静默地把第一个站点的内容吐出去。

匹配方式Nginx 写法Apache 写法优先级
精确匹配server_name a.com;ServerName a.com最高
前置通配server_name *.a.com;ServerAlias *.a.com中
后置通配server_name a.*;不常用低
默认主机default_server第一个 VirtualHost 段兜底

因此,规范的做法是显式声明一个 default_server,把所有未匹配的域名统一拒绝掉,而不是让它落到某个业务站点上。

server { listen 80 default_server; server_name _; return 444; }

return 444是 Nginx 独有的非标准状态码,效果是直接断开连接,不返回任何响应体——对扫描器和错误域名最省资源。

二、Nginx + PHP 8.5 的完整配置

先装 PHP 8.5。Ubuntu 上通常用 ondrej 的 PPA:

sudo add-apt-repository ppa:ondrej/php sudo apt update sudo apt install -y php8.5-fpm php8.5-cli php8.5-mysql php8.5-mbstring php8.5-curl sudo systemctl enable --now php8.5-fpm

装完之后 socket 一般在/run/php/php8.5-fpm.sock。接着为两个站点各写一份 server 块。注意这里刻意不共用 root,root指向各自的public目录:

# /etc/nginx/sites-available/a.com server { listen 80; server_name a.com www.a.com; root /var/www/a.com/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.5-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } location ~ /\.(?!well-known) { deny all; } }

b.com的配置同理,只换server_name、root和 socket。关键点是fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;这一行——如果漏掉,Nginx 会把 PHP-FPM 的默认脚本路径传过去,表现为"Nginx 显示 File not found"、日志里写Primary script unknown。

启用站点并检查语法:

sudo ln -s /etc/nginx/sites-available/a.com /etc/nginx/sites-enabled/ sudo ln -s /etc/nginx/sites-available/b.com /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx

三、Apache + PHP 8.5 的配置方式

Apache 下用mod_proxy_fcgi转发给 PHP-FPM,比老式的mod_php更可控,也能让多个站点跑在不同 FPM 池里。

sudo apt install -y apache2 libapache2-mod-fcgid sudo a2enmod proxy proxy_fcgi setenvif rewrite headers

虚拟主机文件/etc/apache2/sites-available/a.com.conf:

<VirtualHost *:80> ServerName a.com ServerAlias www.a.com DocumentRoot /var/www/a.com/public <Directory /var/www/a.com/public> Options -Indexes +FollowSymLinks AllowOverride All Require all granted </Directory> <FilesMatch "\.php$"> SetHandler "proxy:unix:/run/php/php8.5-fpm-a.sock|fcgi://localhost/" </FilesMatch> ErrorLog ${APACHE_LOG_DIR}/a.com-error.log CustomLog ${APACHE_LOG_DIR}/a.com-access.log combined </VirtualHost>

然后a2ensite a.com b.com,并确认/etc/apache2/ports.conf里只有一个Listen 80(写两次会让 Apache 把请求路由到第一个 vhost)。Apache 的默认主机是配置文件中第一个 VirtualHost 段,所以务必把拒绝用的兜底 vhost 放在最前面,比如新建000-default-deny.conf。

四、PHP-FPM 进程池隔离

多站点共用一个 FPM 池是隐患之源:一个站点的memory_limit调大、open_basedir设宽,会影响所有站点;某个站点打满pm.max_children,其他站点全部排队 502。

正确做法是每个站点一个池。/etc/php/8.5/fpm/pool.d/a.com.conf:

[a.com] user = www-a group = www-a listen = /run/php/php8.5-fpm-a.sock 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 php_admin_value[open_basedir] = /var/www/a.com:/tmp php_admin_value[upload_tmp_dir] = /var/www/a.com/tmp php_admin_value[error_log] = /var/log/php/a.com-error.log php_admin_flag[log_errors] = on php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen

改完sudo systemctl reload php8.5-fpm。注意php_admin_value在 PHP 里无法被 ini_set() 覆盖,这正是隔离的意义所在;如果写成php_value,业务代码一句ini_set('open_basedir', ...)就能绕过。

五、一次性验证脚本

把下面这段存成/var/www/a.com/public/whoami.php,两个站点各放一份,然后分别访问,能立刻确认落在了哪个站、跑了哪个池:

<?php declare(strict_types=1); // 需要 PHP 8.0+(使用了 str_contains 与 match) $host = $_SERVER['HTTP_HOST'] ?? 'unknown'; $root = $_SERVER['DOCUMENT_ROOT'] ?? 'unknown'; $socket = $_SERVER['SCRIPT_FILENAME'] ?? ''; $fpmUser = function_exists('posix_getpwuid') ? (posix_getpwuid(posix_geteuid())['name'] ?? 'n/a') : 'n/a'; header('Content-Type: text/plain; charset=utf-8'); echo "host : {$host}\n"; echo "document_root: {$root}\n"; echo "script : {$socket}\n"; echo "php version : " . PHP_VERSION . "\n"; echo "fpm user : {$fpmUser}\n"; echo "open_basedir: " . (ini_get('open_basedir') ?: '(empty)') . "\n"; echo "sapi : " . PHP_SAPI . "\n"; $flag = match (true) { str_contains($root, '/a.com/') => 'A 站', str_contains($root, '/b.com/') => 'B 站', default => '未识别(可能命中了默认虚拟主机)', }; echo "判定 : {$flag}\n";

访问a.com/whoami.php输出判定: A 站、fpm user: www-a,访问b.com/whoami.php输出判定: B 站,说明名称匹配和池隔离都生效了。如果两个域名的document_root一模一样,那就是典型的"所有请求落到默认主机",回到第一节检查server_name。

常见坑点

1. 只配了server_name,没配ServerAlias,www子域串站

❌ 只写server_name a.com;,结果www.a.com落到默认站点 ✅ 同时列出:server_name a.com www.a.com;

2. 用了$document_root却没在 location 里重设

❌fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;(缺$document_root前缀,FPM 拿到的是相对路径) ✅fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

3. Nginx 的location ~ \.php$被location /的 try_files 抢走

❌ 正则 location 写成了\.php之外的形式,导致.php请求被当静态文件 ✅ 保持正则location ~ \.php$,并在location /里用try_files $uri $uri/ /index.php?$query_string;

4. PHP-FPM 池的用户对站点目录没有读权限,报 502/403

❌ 只改了pool.d里的user,没同步改目录属主 ✅ 池里的user/group必须与chown -R后的站点目录属主一致,否则 FPM 读不到脚本

5.htdocs目录下把源码和可访问目录混在一起

❌root /var/www/a.com;,.env和vendor/直接暴露在公网 ✅root /var/www/a.com/public;,业务代码放在public的上一级

6. 改完配置不reload,或者只reload了 Nginx 没reloadPHP-FPM

❌nginx -s reload之后发现 FPM 池配置没生效 ✅ 两个都要:sudo systemctl reload nginx && sudo systemctl reload php8.5-fpm

7.listen 80 default_server写了两次导致 Nginx 启动失败

❌ 两个 server 块都带default_server,nginx -t报a duplicate default server✅ 整个 Nginx 里default_server只能出现一次,且只给兜底站点

总结

环节关键点出错后的典型症状
域名匹配显式 default_server,server_name列全别名访问 B 站出现 A 站内容
脚本路径SCRIPT_FILENAME必须带$document_rootFile not found/Primary script unknown
站点根目录root指向public,而非项目根配置、源码文件可被直接下载
进程隔离每站一个 FPM 池 +php_admin_value单站崩溃拖垮全部站点,502 频发
生效方式改完必须 reload 两边服务配置"看起来改了但没用"


多站点虚拟主机的核心只有两句话:域名靠Host头精确匹配,匹配不上必须显式拒绝;每个站点要有独立的文档根和独立的 FPM 池。把这两条落地,绝大部分串站、串会话、互相拖垮的问题都会消失。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询