缘由

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