将Nginx与PHP-FPM放到不同容器中运行,是Docker化PHP应用时的常见架构选择。这种做法把负责HTTP层和静态资源的组件,与负责PHP脚本执行的进程管理器彻底解耦,各自拥有独立的镜像、资源限制和发布节奏。从技术原理上讲,Nginx并不解析PHP,它只是通过FastCGI协议把请求转发给FPM监听的端口,因此两者原本就可以运行在不同进程甚至不同主机上,容器化只是把这种物理边界变成了网络边界。

一、分离部署的技术可行性
FastCGI协议本身就是为跨进程、跨主机通信设计的。PHP-FPM默认监听9000端口,等待Nginx通过fastcgi_pass指令将请求抛出。在容器环境中,只要两个容器处于同一个自定义bridge网络,Nginx就可以通过服务名(如php-fpm)加端口访问FPM,不需要任何额外代理软件。这与传统单容器内同时跑Nginx和PHP-FPM相比,只是把本地Unix socket或本地TCP换成了容器间TCP。
从资源调度角度看,分离部署让CPU密集型的PHP执行和IO密集型的Nginx可以分配不同的内存与CPU限额。比如FPM容器崩溃重启不会影响Nginx继续响应静态文件,反之亦然。同时,PHP版本升级只需替换FPM镜像,Nginx镜像保持不动,显著降低了耦合风险。下面是一段最基础的docker-compose定义,展示两者如何连在同一个网络。
version: '3.8'
services:
nginx:
image: nginx:1.25
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./code:/var/www/html:ro
networks:
- web
depends_on:
- php-fpm
php-fpm:
image: php:8.2-fpm
volumes:
- ./code:/var/www/html
networks:
- web
networks:
web:
driver: bridge
1.1 网络通信模型
在自定义bridge网络下,Docker内置的DNS会把服务名解析为容器IP。Nginx配置中的fastcgi_pass php-fpm:9000;就是利用这一点。需要注意的是,FPM容器必须暴露9000端口,但不必映射到宿主机,因为通信完全在内部网络发生。如果误将9000发布到宿主机,反而会增加攻击面。
另一种做法是使用host网络模式,此时容器共享宿主机网络栈,Nginx可直接访问127.0.0.1:9000。但这种模式失去了网络隔离,且不支持多容器同端口,生产环境一般不推荐。绝大多数场景用bridge加服务名即可兼顾安全与简单。
二、共享代码与配置的最佳实践
分离部署最容易踩的坑是代码不同步。Nginx需要读取静态文件做直接返回,PHP-FPM需要读取同一份PHP源码做执行,因此两者必须通过卷挂载同一目录。推荐把业务代码放在宿主机一个目录,同时挂到两个容器的相同路径(如/var/www/html),Nginx挂只读,FPM挂可读写,避免权限混乱。
配置方面,Nginx的fastcgi_param SCRIPT_FILENAME必须指向FPM容器内看到的路径,而不是Nginx自己路径。很多初学者写错路径导致返回“File not found”,其实就是因为两边挂载路径不一致或参数写成了Nginx侧路径。以下Nginx配置片段展示了正确写法:
server {
listen 80;
server_name localhost;
root /var/www/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 /var/www/html$fastcgi_script_name;
include fastcgi_params;
}
}
2.1 环境变量与池配置
PHP-FPM的池配置(www.conf)里常包含pm.max_children等参数,分离部署后建议通过环境变量注入,而不是写死在镜像里。可以用轻量启动脚本读取环境变数并改写配置,这样同一个FPM镜像能根据节点内存动态调整进程数。下面是一个简单的Dockerfile与脚本思路:
FROM php:8.2-fpm COPY docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh ENTRYPOINT ["docker-entrypoint.sh"]
#!/bin/bash
# docker-entrypoint.sh 示例
sed -i "s/pm.max_children = .*/pm.max_children = ${PM_MAX_CHILDREN:-50}/" /usr/local/etc/php-fpm.d/www.conf
exec php-fpm
这种方式让运维在编排文件里直接填environment: PM_MAX_CHILDREN: 80就能生效,不需要重新构建镜像。对于多环境(测试、生产)共用镜像非常友好。
三、健康检查与超时错配处理
容器分离后,必须给PHP-FPM配置健康检查,否则Nginx可能在FPM还没就绪时就转发请求。Docker原生支持healthcheck,可用php-fpm -t或简单TCP探测9000。Nginx侧也可加health_check指令(需商业版或开源patch),或用前端重试掩盖短暂不可用。
另一个高频问题是超时不一致。Nginx默认fastcgi_read_timeout是60秒,而FPM的request_terminate_timeout若设成30秒,会出现FPM已杀进程但Nginx还在等的情况,最终报502。正确做法是FPM终止时间略大于Nginx读取超时,并在Nginx返回504时记录日志定位慢脚本。参考设置如下:
location ~ .php$ {
fastcgi_pass php-fpm:9000;
fastcgi_read_timeout 120s;
include fastcgi_params;
}
; www.conf 片段 request_terminate_timeout = 130s
3.1 日志与排错
分离架构下排错要分别看两边日志。Nginx的error.log会显示连接FPM失败或上游超时;FPM的slow.log能抓出执行慢的脚本。建议把两个容器的日志都挂载到宿主机或用集中采集,避免容器重建丢失现场。遇到“connect() failed (111: Connection refused)”基本就是网络或FPM未启动,遇到“Primary script unknown”则是路径映射错误。
总体来看,Nginx与PHP-FPM容器分离不仅完全可行,而且是微服务化PHP应用的推荐形态。只要把握住网络互通、代码同挂、超时协调三点,就能在获得独立伸缩与版本隔离的同时,不引入难以承受的运维负担。
NginxPHP-FPMdocker_compose修改时间:2026-08-05 04:36:32