用 caddy 代替 nginx, 一行配置搞定 php-fpm 反向代理
缘由
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 感兴趣了?
一. 云原生
-  
  
单文件静态链接. 我在打包 PHP 应用的时候, 如果用 caddy, copy 一个文件即可. 不需要考虑各种动态链接和依赖.  -  
  
自带 metrics ( https://caddyserver.com/docs/metrics)  -  
  
自带 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指定站点根目录.
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 的代码并不了解,我的想法是"解决问题", 所以我的理解是,简单地增加两个选项:
-  
  
always_proxy_to_index (
try_files {path} {path}/index.php index.php, off 的时候去掉最后面的index.php) -  
  
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
 }
}
 
不得不说, 这样实现, 使得整个配置变得更优雅. 功能增加了, 但是并没有增加新的配置指令和选项.
思考
-  
  
使用开源产品时,遇到了问题,不要抱怨, 要有耐心,积极参与讨论.  
如果一个东西不完善, 并且你发现了这一点, 积极地提出来, 说不定开发者就采纳了, 改进了. 不单是你, 其它有同样需求的人也会受益.
-  
  
实现一个东西的时候, 不要只考虑解决当前问题而导致代码膨胀, 而是要看到问题的本质, 以更优雅地方式去解决.  
最后, 提一下, caddy 与 php 集成的时候, 还有一些性能优化相关的问题没解决, https://github.com/caddyserver/caddy/issues/3803
