写在前面
运行一个php站点最简单的办法是,直接运行一个nginx+php或apache+php的docker镜像组,或者直接开一台虚机直接安装nginx+php。
本文情况特殊,由于想用本地nginx+容器php,且避免二次反向代理,所以踩了一些坑,在此记录一下。新手建议还是先从简单的方案上手,而不是本文的方案。
背景
已有一台机器,本地已经运行nginx(使用systemd运行),现在需要增加服务一个php站点,并有要求将php-fpm在docker中运行。所以需要配置docker容器,并配置nginx vhost主机。
一些事实
- 一个php站点需要由两部分才能组成:
- web服务器:用于反向代理、负载均衡、静态资源分发(如站点中图片、html等除.php文件资源,文件本身就是响应)
- php-fpm / php-cgi 进程:站点中的php文件实际上是脚本,脚本运行后的结果才是要返回客户端的响应,所以需要web服务器将请求发到php,获得运行结果后,web服务器再将响应返回到客户端。
- 大部分情况下,一个php站点中,静态资源和php脚本通常是混合存放的。所以web服务器和php-fpm这两个程序通常都需要访问同一个站点资源目录。请求到达web服务器后,web服务器将判断客户实际请求的是普通的静态资源,还是一个php脚本。如果是后者,则web服务器将通过fastcgi协议调用php,调用时,需要指定一系列参数,其中最重要的一个就是
SCRIPT_FILENAME
参数,这个参数告诉了php本次要运行的是哪一个脚本,你需要确保这个参数指向的文件可被php进程读取。 - 在本文中,web服务器使用nginx(本地systemd运行),php使用docker容器(Bitnami/php-fpm)。关于fpm和cgi的区别可以网络搜索,一般使用fpm更通用。
操作过程
- 创建存放php站点文件的目录。
从前提部分可知,需要一个公共的目录,以同时供web服务器和php服务访问。此处,可以指定nginx默认的html目录(如/usr/share/nginx/html
或/var/www/html
等,取决于不同的发行版)。
由于此php站点计划使用专门的域名提供服务,所以新建一个文件夹作为站点文件目录。
mkdir /usr/share/nginx/php-site/
- 将文件拷贝到站点目录
此处仅作演示目的,创建一个php文件和一个html文件。
mkdir /usr/share/nginx/php-site/app01
echo '<?php echo "The time is " . date("h:i:sa"); ?>' > /usr/share/nginx/php-site/app01/index.php
echo '<html><body><h1>hello</h1></body></html>' > /usr/share/nginx/php-site/app01/example.html
- 创建php docker容器
docker run -d --name=phpfpm\
-e TZ=Asia/Shanghai \
-p 9000:9000 \
-w /usr/share/nginx/php-site \
-v /usr/share/nginx/php-site:/usr/share/nginx/php-site \
bitnami/php-fpm:latest
-w参数也可以不加,这个参数在这个镜像中只影响你docker exec的时候默认会进到哪个目录。
-v挂载的目录/usr/share/nginx/php-site
必须是第1步中创建的路径,具体看结尾解释
- 创建一个nginx vhost
/etc/nginx/sites-enabled/m01.example.com.conf
# include /etc/nginx/conf.d/php-fpm.conf; # 此文件已在http块引入
server {
listen 80;
#listen [::]:80; # 监听IPv6 80端口
server_name m01.example.com;
root /usr/share/nginx/php-site;
location / {
try_files $uri $uri/index.php;
}
include /etc/nginx/default.d/php.conf; # 发行版自带的的php配置文件,后文贴出
}
此外,还需要指定一个php-fpm的upstream,此处直接修改自带配置文件,注意此文件已经在http块中引入:
/etc/nginx/conf.d/php-fpm.conf
:
# PHP-FPM FastCGI server
# network or unix domain socket configuration
upstream php-fpm {
# 注释下面这行,nginx默认假设phpfpm是本地socket连接的,不适合本文情况
# server unix:/run/php-fpm/www.sock; #
server 127.0.0.1:9000;
}
更新完成后,执行nginx -t
测试配置文件,若无错误,执行nginx -s reload
。
附:自带配置文件:/etc/nginx/default.d/php.conf
# pass the PHP scripts to FastCGI server
#
# See conf.d/php-fpm.conf for socket configuration
#
index index.php index.html index.htm;
location ~ \.(php|phar)(/.*)?$ {
fastcgi_split_path_info ^(.+\.(?:php|phar))(/.*)$;
fastcgi_intercept_errors on;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass php-fpm;
}
附:自带配置文件:/etc/nginx/fastcgi_params
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;
- 测试运行
使用curl http://m01.example.com/app01/example.html
可以获得响应hello,证明html文件serve正常;
使用curl http://m01.example.com/app01/
可以获得当前时间响应,说明php文件serve正常。
错误排除
- Primary script unknown 错误
日志内容:
[error] 21029#21029: *998 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream, client: 1.2.3.4, server: m01.example.com, request: "GET /app01/ HTTP/2.0", upstream: "fastcgi://127.0.0.1:9000"
这一般来说是由于fastcgi调用php时传递的SCRIPT_FILENAME
参数存在错误产生的。搜索资料可知,文件不存在、文件权限错误都会导致产生这个问题,本文中php在容器中为root用户,不存在权限问题,所以100%为文件不存在,或参数错误。
调试方法是修改nginx.conf,将error_log /var/log/nginx/error.log error;
一行最后的error
改为debug
,然后执行nginx -sreload
,然后观察/var/log/nginx/error.log
文件中的debug信息。(生产环境谨慎开启debug,建议使用server块级别的error配置)
搜索SCRIPT_FILENAME
找到这行fastcgi调用参数的打印:
2024/09/21 17:55:50 [debug] 21024#21024: *908 fastcgi param: "SCRIPT_FILENAME: /usr/share/nginx/php-site/app01/index.php"
然后进入到php容器(如docker exec),确认一下这个文件是否存在。此处文件已经通过docker -v挂载,所以访问正常。
总结
很久没有用过php,踩坑的原因是对web服务、php服务协作方式不够了解,对SCRIPT_FILENAME
参数理解不够,也不了解fastcgi调用参数的debug方法。
实际上,SCRIPT_FILENAME
参数怎么设置都可以,只要在php容器中存在这个参数指向的文件,就能正常运行到php脚本。
但这个参数默认情况下都是被设置为$document_root$fastcgi_script_name
,所以web服务的document_root需要设置为php资源的目录,且php容器挂载目录时需要和document_root路径一致不能修改。设置不对就会导致报错404或者返回一个File not found.
如果一定想要不一致,你只需要:
- 将
SCRIPT_FILENAME
设置为/app$fastcgi_script_name
; - 挂载时指定
-v /usr/share/nginx/php-site:/app
关键词
php-fpm nginx docker script_filename