在 Docker 容器环境中,Nginx 与 PHP-FPM 通常被拆分到两个独立容器中运行,这样既便于扩展,也能让不同服务使用不同镜像。但很多开发者在完成容器编排、启动服务后,访问 PHP 页面时却遇到 502 Bad Gateway,或者浏览器没有执行 PHP 脚本,而是直接下载 index.php 文件。这些现象并不代表业务代码有问题,更多时候是因为容器间的网络连接没有打通、FastCGI 参数配置不正确,或者两个容器内的文件路径不一致。下面从容器网络、FPM 监听地址、脚本路径映射以及权限日志四个方面,逐一说明如何定位和解决这类故障。

一、容器网络与 fastcgi_pass 地址
出现 502 的最常见原因是 Nginx 配置里使用了 fastcgi_pass 127.0.0.1:9000;。在传统单机部署中,Nginx 和 PHP-FPM 位于同一台主机,回环地址可以正常通信。但在 Docker 容器化架构下,每个容器都有独立的网络命名空间,127.0.0.1 只代表 Nginx 容器自身,不会指向运行 PHP-FPM 的另一个容器。因此,如果仍然使用回环地址,Nginx 的连接请求只能到达自己内部,PHP-FPM 根本收不到任何数据,最终返回 502。
解决这个问题需要将两个容器放入同一个自定义 bridge 网络,并且让 Nginx 通过服务名或容器名来访问 PHP-FPM。在 docker-compose 中,可以声明一个 networks 区块,比如名为 appnet,然后将 nginx 和 php-fpm 服务都加入该网络。此时 Nginx 的 fastcgi_pass 应该写作 php-fpm:9000,这里的 php-fpm 是 compose 中的服务名。Docker 内置的 DNS 服务会把服务名解析为对应容器的 IP 地址,即使容器重启后 IP 发生变化,也能自动更新解析结果。
如果使用 docker run 手动启动容器,则需要先创建网络,例如 docker network create mynet,然后启动两个容器时都添加 --network mynet 参数,并且确保 PHP-FPM 容器的名称与 Nginx 配置中 fastcgi_pass 使用的主机名一致。网络建立后,可以进入 Nginx 容器,用 getent hosts php-fpm 或 ping php-fpm 来验证名称解析是否正常。网络层连通是后续所有配置生效的前提。
version: '3'
services:
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./code:/var/www/html
networks:
- appnet
php-fpm:
image: php:fpm-alpine
volumes:
- ./code:/var/www/html
networks:
- appnet
networks:
appnet:
driver: bridge
二、PHP-FPM 监听地址配置
官方提供的 php:fpm 镜像默认会监听 9000 端口,并且绑定到 0.0.0.0,这意味着容器外可以通过容器 IP 或服务名访问,符合跨容器通信的需求。但很多开发者会挂载自定义的 www.conf 配置文件,其中可能包含 listen = /run/php/php-fpm.sock 这样的 Unix socket 配置。Unix socket 主要用于同一主机内进程间通信,在 Docker 容器之间是完全无法使用的,因为每个容器有独立的文件系统视图,/run/php/php-fpm.sock 只存在于 PHP-FPM 容器内部,Nginx 容器无法访问该路径,最终导致连接被拒绝。
要修复此类问题,需要把 PHP-FPM 的监听地址改为 TCP 套接字,即 listen = 0.0.0.0:9000。同时检查 listen.allowed_clients 是否限制了来源地址,如果只允许特定 IP 访问,则应根据实际情况放行,或者直接设置为 any 以便在开发环境中快速验证。修改完成后需要重启 PHP-FPM 容器使配置生效。
启动后不要立即测试页面,而是先从网络层确认端口连通性。可以执行 docker exec 进入 Nginx 容器,使用 nc -zv php-fpm 9000 或 telnet php-fpm 9000 查看连接是否被接受。如果返回成功,说明 Nginx 容器已经可以通过服务名访问到 FPM 的监听端口,网络与监听配置基本就绪。否则需要回到网络配置和 FPM 监听参数继续排查。
; PHP-FPM 池配置片段 [www] listen = 0.0.0.0:9000 listen.allowed_clients = any user = www-data group = www-data
三、Nginx FastCGI 脚本路径映射
当网络连通、端口监听正常后,如果仍然出现 Primary script unknown 错误,通常是因为 FastCGI 脚本路径映射错误。Nginx 接收到 PHP 页面请求后,会通过 FastCGI 协议把请求转发给 PHP-FPM,同时传递一个名为 SCRIPT_FILENAME 的参数,这个参数指示 PHP-FPM 应该在哪个文件系统路径下查找并执行对应的 PHP 文件。由于 Nginx 和 PHP-FPM 是两个独立容器,它们分别拥有各自的文件系统,因此 Nginx 配置中的 root 路径和传递给 FPM 的脚本路径必须与 PHP-FPM 容器内实际代码存放位置完全一致。
常见的正确配置是,宿主机上的代码目录同时挂载到 Nginx 和 PHP-FPM 容器的 /var/www/html 路径。Nginx 的 root 设置为 /var/www/html,并在处理 PHP 的 location 块中使用 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 这样的写法,让 Nginx 自动把根路径和脚本名拼成完整路径。也可以直接写死 fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;,但使用 $document_root 更容易维护。
需要特别注意的是,两个容器对代码目录的挂载路径必须完全相同。如果只给 Nginx 挂载了代码目录,而没有给 PHP-FPM 挂载,或者一个挂载到 /var/www/html,另一个挂载到 /app/code,就会导致 PHP-FPM 在预期路径下找不到文件,从而返回脚本未知错误。另外,在 Nginx 中配置 try_files 可以避免 index.php 被当作普通文件下载,确保所有无法匹配静态文件的请求都交给 index.php 处理。
server {
listen 80;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ .php$ {
fastcgi_pass php-fpm:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
四、权限与日志排查
如果网络、监听、路径这三项配置都正确,但页面仍然无法正常加载,就需要查看日志来定位更深层的问题。Nginx 的错误日志通常位于 /var/log/nginx/error.log,在容器环境中也可以直接使用 docker logs nginx_container_name 查看标准输出或文件日志。PHP-FPM 的日志可以查看容器输出,或者进入容器检查 FPM 日志文件。日志中通常会给出明确的错误原因,例如权限拒绝、文件不存在、上游连接超时等。
权限问题是容器化 PHP 部署中容易被忽视的一环。PHP-FPM 默认使用 www-data 用户运行,如果宿主机挂载的代码目录或文件权限设置过严,例如目录为 700 或文件为 600,并且所有者不是 www-data,FPM 进程就会因为无法读取脚本而报错。一种常见的修复方式是把宿主机上的代码目录权限设置为 755,文件权限设置为 644,或者在 compose 文件中通过 user: "1000:1000" 指定 FPM 进程以宿主机当前用户的 UID 和 GID 运行,从而与挂载目录的所有者保持一致,避免因权限不匹配导致读取失败。修改后需要重启 PHP-FPM 容器:
docker compose restart php-fpm
重启后再次查看日志,如果原来的权限错误消失,说明问题已经解决。若仍然存在,可以进入容器手动检查目录和文件权限:
docker exec -it php_fpm_container sh -c 'id && ls -l /var/www/html'
需要注意,如果宿主机启用了 SELinux,容器访问挂载目录时还可能受到安全上下文限制。此时可以在 compose 文件的卷挂载末尾添加 :z 或 :Z,让 Docker 自动重新标记目录,例如:
volumes: - ./code:/var/www/html:z
不过 :z 会共享标签,:Z 则更严格地私有化,使用时需要根据业务场景选择。
在 Nginx 的错误日志中,以下几条信息可以帮助快速判断:
FastCGI sent in stderr: "Primary script unknown" connect() failed (111: Connection refused) while connecting to upstream upstream timed out (110: Connection timed out) while reading response header from upstream
Primary script unknown 通常表示 Nginx 传递给 PHP-FPM 的脚本路径错误,fastcgi_param SCRIPT_FILENAME 中的 $document_root 与 PHP-FPM 容器内的实际路径不一致。Connection refused 多出现在 PHP-FPM 容器未启动、未监听 9000 端口,或者 Nginx 配置的 fastcgi_pass 地址指向了错误的主机或端口。upstream timed out 则与 PHP 脚本执行时间过长或后端响应缓慢有关,可以适当调大 fastcgi_read_timeout,同时检查 PHP 脚本是否存在死循环或外部请求阻塞。
五、容器网络与最终检查
如果日志中没有明显错误,但请求仍然异常,还要确认容器之间的网络是否正常。使用自定义网络时,容器名可以直接作为主机名解析。可以通过以下命令测试 Nginx 容器到 PHP-FPM 容器的连通性:
docker exec -it nginx_container_name sh -c 'nc -zv php-fpm 9000'
如果输出 succeeded 或端口开放,说明网络链路正常;如果解析失败,则需要检查两个容器是否加入了同一个 Docker 网络,或者 compose 文件中是否正确定义了 depends_on 和 networks。
至此,从容器状态、端口监听、路径映射、权限日志到网络连通性的排查思路已经形成闭环。日常维护中建议将 Nginx 与 PHP-FPM 的配置纳入版本控制,并在修改后通过 docker compose config 检查配置语法,再用 docker compose up -d 平滑重建容器。遇到 502、404、Permission denied 等问题时,先看容器状态,再看日志,最后核对路径与权限,通常能快速定位并解决大部分 PHP 容器化部署问题。