6

用 caddy 代替 nginx, 一行配置搞定 php-fpm 反向代理

 2 years ago
source link: https://ttys3.dev/post/setup-caddy-as-php-fpm-reverse-proxy-server-with-one-line-config/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

October 5, 2021

缘由

caddy 其实并不是一个新项目, 虽然早在 caddy 1.x 版本的时候老灯就关注这个项目了, 但是,老灯其实最近才开始去了解它和尝试使用它.

为什么之前一直没用 caddy 呢? 一是当时 nginx 还是非常坚挺, 可以说在 nginx + php-fpm 这一搭配方面, 基本上无敌手. 二是, 当时我试用过 Caddyfile 后,发现它太简洁了,简洁到我以配置 nginx 的思维, 完全无法适应配置 Caddyfile. 没错, 我当时觉得 , 配置 nginx 比 Caddyfile 简单多了 (因为 nginx 配置已经熟练多年).

从提交记录看, https://github.com/caddyserver/caddy/blob/v1/LICENSE.txt 最早可以追溯到2015年.

简单来说, Caddy 是一个使用 Golang 编写的 http 静态文件服务器和 反向代理服务器.

大概一年前,作者重构了 caddy, 也就是现在的 caddy 2.x 版. HN 上面依然可以找到他宣布 caddy 2 出来的消息:

mholt on May 4, 2020 [–]

Hi HN – this is what I’ve been working on for the last 14 months, with the help of many contributors and the backing of several sponsors. (Thank You!) Caddy 2 is a fresh new server experience. Some things might take getting used to, like having every site served over HTTPS unless you specify http:// explicitly in your config. But in general, it will feel familiar to v1 in a lot of ways. If you’ve used v1 before, I recommend going into v2 with a clean-slate using our Getting Started guide: https://caddyserver.com/docs/getting-started

I’m excited to finally get this out there. Let me know what questions you have!

via https://news.ycombinator.com/item?id=23070567

那么, 为什么现在对 Caddy 感兴趣了?

一. 云原生

  1. 单文件静态链接. 我在打包 PHP 应用的时候, 如果用 caddy, copy 一个文件即可. 不需要考虑各种动态链接和依赖.
  2. 自带 metrics ( https://caddyserver.com/docs/metrics)
  3. 自带 REST api ( https://caddyserver.com/docs/api)

二. 作为 Golang 程序员, 对于基于 Go 实现的东西, 有一种天生的好感.

Caddyfile 结构

下图取自官方文档: https://caddyserver.com/docs/caddyfile/concepts#structure

整个配置文件按块划分.

第一个红色的大 block 是全局配置块, 如果存在,它必须放在第一个块的位置.

每个 option 的配置, 基本上是 key - value 或者 key - list 的形式.

每个 snippet 也是单独成为一个 block, snippet 可以方便相同的配置复用,避免手动 copy 配置使得同样的配置分布在多处而难以维护.

每个站点单独成为一个 block.

这个配置足够简洁, 对于新手也极其友好.

关于配置这一块, 想要进一步了解的,可以查看官方文档, 同时推荐读一下 飞雪无情写的 Caddy实战(十一)| Caddyfile 设计之美

配置 php-fpm 反向代理

从官方文档点击 “ Common Patterns”, 然后其中就有 关于 PHP 的说明.

配置一个 caddy + php-fpm 的博客, 全部指令如下:

example.com

root * /var/www
php_fastcgi /blog/* localhost:9000
file_server

example.com指定域名(默认监听https),

root * /var/www指定站点根目录.

php_fastcgi /blog/* localhost:9000 表示所有匹配 /blog/*的请求的 *.php 文件, 都被代理到 php-fpm 的监听地址 (localhost:9000)

如果要使用 unix socket 也是支持的, 将 localhost:9000替换成相应的 socket 地址即可, 如 unix//run/php/php8.0-fpm.sock

file_server 用于serve静态文件.

404问题

没错, 上面的配置, 在大部分情况下是工作的. 对于现在一些基于流行 PHP 框架的应用, 比如 基于 Symfony / Laravel / CodeIgniter / CakePHP / Phalcon 等, 大多数是以单一入口来跑的. 对于单一入口的应用, 404 错误都是交给应用程序层面来处理的, 而不是 http 服务器.

对于非单一入口应用程序, 典型的是, 你手里有一个几年前的程序, 没有用任何框架, 这个时候, caddy 的默认配置, 404 的问题就出来了.

the problem

这个问题直接描述比较抽象, 所以我这里举例子说明. 假设配置如下:

# listen http and https both for example.com
{$SITE_ADDRESS:http://example.com, https://example.com} {
	root * /app/public

	# https://caddyserver.com/docs/caddyfile/directives/php_fastcgi
	php_fastcgi 127.0.0.1:9000

	file_server

	handle_errors {
        root * /etc/caddy/error
		rewrite * /error.html
		templates
		file_server
	}
}

本意是, 如果有 404 或 403 等错误, 由 /error.html来处理, 但是实际上访问不存在的 url, 如

/this-is-not-found.html
/blah-this-is-another-notfound/xxxxxx
/xxxxxxx
/sub/xxx/sub/xxxxx

404 错误并没有发送给 handle_errors 这个 handler 来处理, 而是直接被 php index.php 处理了. 由于这个程序并不是单一入口应用, 因此它的 index.php 完全不知道如何错误 404 错误请求的 uri.

尝试解决

然后我继续查看 Caddy 文档, 发现 php_fastcgi 有一个 Expanded form , 我尝试把其中的 try_files {path} {path}/index.php index.php 修改为 try_files {path} {path}/index.php 之后,大部分 404 错误解决了. 但是以 .php 结尾的文件的 404 错误还是无法处理.

经过修改的完整的配置如下:

# listen http and https both for example.com
{$SITE_ADDRESS:http://example.com, https://example.com} {
	root * /app/public

	# https://caddyserver.com/docs/caddyfile/directives/php_fastcgi
	# php_fastcgi 127.0.0.1:9000
	route {
		# Add trailing slash for directory requests
		@canonicalPath {
			file {path}/index.php
			not path */
		}
		redir @canonicalPath {path}/ 308

		# If the requested file does not exist, try index files
		@indexFiles file {
			# try_files {path} {path}/index.php index.php
			try_files {path} {path}/index.php
			split_path .php
		}
		rewrite @indexFiles {http.matchers.file.relative}

		# Proxy PHP files to the FastCGI responder
		@phpFiles path *.php
		reverse_proxy @phpFiles 127.0.0.1:9000 {
			transport fastcgi {
				split .php
			}
		}
	}

	file_server


	handle_errors {
        root * /etc/caddy/error
		rewrite * /error.html
		templates
		file_server
	}
}

目前这个配置还存在的问题, 无法处理 .php 结尾文件的 404 错误, 如

/this-is-no-fount.php
/sub1/sub2/sub3/xxxxx.php

这种并不存在的 php 文件, 还是会被代理到 php-fpm 进程处理, 然后触发 php-fpm 响应 " No input file specified. “ 的错误, 这个错误并不友好, 也不是我们期望的处理方式.

为了避免 ” No input file specified. “ 错误, 在 nginx 里面我们一般只需要这样处理:

	# Pass the php scripts to fastcgi server specified in upstream declaration.
	location ~ \.php(/|$) {
		# avoid No input file specified
		try_files  $uri =404;
		
		include fastcgi_params;
		
		# the missing param in fastcgi_params copied from fastcgi.conf
		fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
		
		fastcgi_pass 127.0.0.1:9000;

		fastcgi_buffer_size   64k;
		fastcgi_buffers    8 256k;
		fastcgi_busy_buffers_size 256k;
	}

没错, 神奇的地方就在于 try_files $uri =404; 这一行配置.

由于我其实等于是刚看了5分钟文档, 之前并没有真正使用 caddy, 因此我也不知道如何处理这一情况. 于是我把 这个问题发到 github 上面了.

然后其中一个热心开发者 Francis Lavoie 马上给出了解决方案:

Francis Lavoie 觉得这是一个好问题. 他建议用一个 error 指令来解决:

# Trigger error for non-existing PHP scripts
@phpNotFound {
    path *.php
    not file
}
error @phpNotFound 404

这样, 不存在的 php 文件就会触发 404 错误, 不会继续交给 php-fpm 处理.

更好的实现方案

使用 error 指令 加 Expanded form 确实能解决问题. 但是这是一个典型的应用场景, 应该还可以使配置更简化.

由于我对于 caddy 的代码并不了解,我的想法是"解决问题”, 所以我的理解是,简单地增加两个选项:

  1. always_proxy_to_index ( try_files {path} {path}/index.php index.php , off 的时候去掉最后面的 index.php)

  2. pass_to_error_handler (遇到404等错误时,抛给其它 handler, 如 handle_errors)

但是, Francis Lavoie 的理解显然更加透彻, 他认为增加两个狭义的选项(使用场景非常有限), 容易使配置越来越复杂(后面要的功能更多,情况越复杂,这样的选项会越来越多). 于是便 有了这两个 PR

Add support for triggering errors from try_files #4346 https://github.com/caddyserver/caddy/pull/4346

Implement try_files override in Caddyfile directive #4347 https://github.com/caddyserver/caddy/pull/4347

大约一周过去了, 这两个 PR 都被   mholt (caddy 创始人和主要开发者) review 并合并进去了.

第一个 PR 其实就是给 try_files 实现了 类似 nginx =404 这种功能.

第二个 PR 是允许对 try_files 进行 override. 这样 php_fastcgi 默认的try_files 就可以被自定义了.

这两个提交预计会在 caddy 2.5.0 版本发布.

到时候, 我的配置就可以简写为:

# listen http and https both for example.com
{$SITE_ADDRESS:http://example.com, https://example.com} {
	root * /app/public

	# https://caddyserver.com/docs/caddyfile/directives/php_fastcgi
	php_fastcgi localhost:9000 {
		try_files {path} {path}/index.php =404
	}

	file_server

	handle_errors {
        root * /etc/caddy/error
		rewrite * /error.html
		templates
		file_server
	}
}

不得不说, 这样实现, 使得整个配置变得更优雅. 功能增加了, 但是并没有增加新的配置指令和选项.

思考

  1. 使用开源产品时,遇到了问题,不要抱怨, 要有耐心,积极参与讨论.

如果一个东西不完善, 并且你发现了这一点, 积极地提出来, 说不定开发者就采纳了, 改进了. 不单是你, 其它有同样需求的人也会受益.

  1. 实现一个东西的时候, 不要只考虑解决当前问题而导致代码膨胀, 而是要看到问题的本质, 以更优雅地方式去解决.

最后, 提一下, caddy 与 php 集成的时候, 还有一些性能优化相关的问题没解决, https://github.com/caddyserver/caddy/issues/3803


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK