docker运行php-fpm,并使用本地nginx反向代理

写在前面

运行一个php站点最简单的办法是,直接运行一个nginx+php或apache+php的docker镜像组,或者直接开一台虚机直接安装nginx+php。

本文情况特殊,由于想用本地nginx+容器php,且避免二次反向代理,所以踩了一些坑,在此记录一下。新手建议还是先从简单的方案上手,而不是本文的方案。

背景

已有一台机器,本地已经运行nginx(使用systemd运行),现在需要增加服务一个php站点,并有要求将php-fpm在docker中运行。所以需要配置docker容器,并配置nginx vhost主机。

一些事实

  1. 一个php站点需要由两部分才能组成:
    • web服务器:用于反向代理、负载均衡、静态资源分发(如站点中图片、html等除.php文件资源,文件本身就是响应)
    • php-fpm / php-cgi 进程:站点中的php文件实际上是脚本,脚本运行后的结果才是要返回客户端的响应,所以需要web服务器将请求发到php,获得运行结果后,web服务器再将响应返回到客户端。
  2. 大部分情况下,一个php站点中,静态资源和php脚本通常是混合存放的。所以web服务器和php-fpm这两个程序通常都需要访问同一个站点资源目录。请求到达web服务器后,web服务器将判断客户实际请求的是普通的静态资源,还是一个php脚本。如果是后者,则web服务器将通过fastcgi协议调用php,调用时,需要指定一系列参数,其中最重要的一个就是SCRIPT_FILENAME参数,这个参数告诉了php本次要运行的是哪一个脚本,你需要确保这个参数指向的文件可被php进程读取。
  3. 在本文中,web服务器使用nginx(本地systemd运行),php使用docker容器(Bitnami/php-fpm)。关于fpm和cgi的区别可以网络搜索,一般使用fpm更通用。

操作过程

  1. 创建存放php站点文件的目录。
    从前提部分可知,需要一个公共的目录,以同时供web服务器和php服务访问。此处,可以指定nginx默认的html目录(如/usr/share/nginx/html/var/www/html等,取决于不同的发行版)。
    由于此php站点计划使用专门的域名提供服务,所以新建一个文件夹作为站点文件目录。
mkdir /usr/share/nginx/php-site/
  1. 将文件拷贝到站点目录
    此处仅作演示目的,创建一个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
  1. 创建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步中创建的路径,具体看结尾解释

  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;
  1. 测试运行
    使用curl http://m01.example.com/app01/example.html可以获得响应hello,证明html文件serve正常;
    使用curl http://m01.example.com/app01/ 可以获得当前时间响应,说明php文件serve正常。

错误排除

  1. 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.

如果一定想要不一致,你只需要:

  1. SCRIPT_FILENAME设置为/app$fastcgi_script_name;
  2. 挂载时指定-v /usr/share/nginx/php-site:/app

关键词

php-fpm nginx docker script_filename

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容