Caddy Server 是一个模块化且现代化的 Web 服务器平台,支持自动 HTTPS 证书、QUIC 和 HTTP/2、Zstd 和 Brotli 压缩,以及多种现代功能,同时也具备经典 Web 服务器的功能,如可配置的虚拟主机、URL 重写和重定向、反向代理等。

Caddy 2 是于 2020 年 5 月发布的当前版本,它在配置语法、自动化、插件等方面引入了重大改进。

本文介绍如何将 PHP 与 Caddy Web 服务器 2 系列集成,并进行高级配置。同时,还将比较类似 Apache 和 Nginx 的配置,以简化从 Apache 和 Nginx 迁移到 Caddy 的过程。

初始服务器设置

Caddy 可在许多操作系统和基于 Linux 的发行版上运行。Caddy 文档解释了如何安装 Caddy,并将其配置为随服务器启动自动运行的服务/守护进程。

安装 Caddy 后,可以使用最小化配置进行设置:如果存在静态文件则提供这些文件,否则将其他请求传递给 PHP-FPM。

example.com 是本文其余部分使用的示例域名,其源文件放置在 /var/www/example.com 中,其中 /var/www/example.com/public/index.php 作为 Web 应用程序的入口点。Web 应用程序的所有资源也存储在 /var/www/example.com/public 目录中,但应用程序的其他部分(包括源文件、Composer vendor 目录、测试、composer.json 文件、NPM node_modules 目录等)位于 /var/www/example.com。

Caddy Server 自带安全且高性能的默认配置,使其易于通过最小化配置进行设置。

当 Caddy 被安装并配置为系统服务时,可以使用默认的 /etc/caddy/Caddyfile 作为全局配置文件,并使用建议名称的子目录 /etc/caddy/sites 来存放各个站点的配置文件,类似于 Apache 和 Nginx 的配置方式。

/etc/caddy
  ├── Caddyfile
  ├── config/
  │     └── php-fpm.conf
  └── sites/
        └── example.com.conf

全局 Caddyfile 可以指定全局配置,并包含 config/* 和 sites/* 目录以引入额外的配置。

Caddyfile

{  
    log default {  
       format console  
       output file /var/log/caddy/system.log  
       exclude http.log.access  
    }
}

import config/*  
import sites/*

此配置将 Caddy 设置为将系统日志写入 /var/log/caddy/system.log 文件(但不包括 HTTP 请求日志),并从 config 和 sites 目录加载额外的配置文件。

运行、启动、停止和重载 Caddy Server

如果 Caddy 已作为 systemd 服务安装,可以使用 systemctl 命令来启动、停止、重载和重启 Caddy 服务器。

对于临时配置,可以使用 caddy 命令控制服务器:

systemctl start caddy
caddy start
caddy run # 启动服务器并无限期阻塞

systemctl stop caddy
caddy stop

systemctl reload caddy
caddy reload

systemctl restart caddy
caddy stop && caddy start

将 Caddy 与 PHP-FPM 集成

与 Apache Web 服务器和 Nginx 集成 PHP 的方式类似,Caddy 也使用 Caddy 的 FastCGI 反向代理与 PHP 集成。

基本思路是:当 Caddy 接收到需要使用 PHP 处理的请求(例如,针对带有 .php 扩展名的文件的请求)时,该请求会被发送到 PHP-FPM,在那里执行 PHP 应用程序,然后将响应返回给 Caddy 再返回给用户。

在最简单的情况下,以下是一个完全可用的 Caddy 站点定义:

/etc/caddy/sites/example.com.conf

example.com {

    root * /var/www/example.com/public

    log {
        output file /var/log/caddy/example.access.log
        format console
    }

    # Encode responses in zstd or gzip, depending on the
    # availability indicated by the browser.
    encode zstd gzip

    # Configures multiple PHP-related settings
    php_fastcgi unix//run/php/php-fpm.sock

    # Prevent access to dot-files, except .well-known
    @dotFiles {  
      path */.*  
      not path /.well-known/*  
    }
}

上述配置文件是一个最小化的完整配置文件,它处理了多个安全和性能方面。

有关此处使用的指令的完整列表及其详细信息,请参阅优秀的 Caddy 文档。

Caddy 为 PHP 提供的额外便利:php_fastcgi

Caddy 在将自身与 PHP 集成方面提供了额外的便利。便捷的 php_fastcgi 指令是多个配置选项的简写,它将请求传递给 PHP-FPM,尝试从当前目录加载 index.php,最后将所有请求重写为根目录下的 index.php。这种模式通常被称为“前端控制器模式”,可用于绝大多数 PHP 框架和 CMS,包括 Laravel、Drupal、WordPress、Slim PHP 等。

在最简单的形式下,php_fastcgi 接受一个参数用于指定 PHP-FPM 服务器。它可以是服务器地址和端口(例如 127.0.0.1:9000)或 Unix 域套接字地址。在 Debian/Ubuntu/衍生版和 RHEL/Fedora/衍生版上,这几乎总是以 Unix 套接字的形式可用,路径模式通常为 /run/php/php[VERSION]-fpm.sock。例如,对于 PHP 8.2,套接字地址为 /run/php/php8.2-fpm.sock,而 PHP 8.3 则为 /run/php/php8.3-fpm.sock。

如果使用 Unix 域套接字不可行,请使用 IP 地址和端口号。

PHP-FPM 监听的 Unix 域套接字地址或 IP/端口可以在 PHP-FPM 配置文件中配置。

URL 重写至 index.php

默认情况下,php_fastcgi 隐含三个重要的 URL 重写规则:

1. 如果存在 ./index.php 文件,则将请求重写为该文件

如果请求到达 example.com/test,且 test/index.php 处存在文件,Caddy 会将此请求重写为 test/index.php 文件。这解决了“尾部斜杠问题”,即 PHP 应用程序存在于文档根目录的子目录中。

2. 如果文件不存在,则重写为 index.php

php_fastcgi 配置的第二个功能是:如果文件存在,Caddy 会尝试直接提供该文件。例如,如果用户请求 example.com/image.png,且文档根目录中存在名为 image.png 的文件,Caddy 将直接提供该文件,完全不调用 PHP。

然后,如果文件不存在,它会尝试在路径中将 index.php 视为目录查找,随后尝试将其重写为根目录下的 index.php。

此步骤类似于以下 Apache 配置:

RewriteCond %{REQUEST_FILENAME} !-f  
RewriteCond %{REQUEST_FILENAME} !-d  
RewriteRule ^ index.php [QSA,L]

... 以及以下 Nginx 配置:

try_files $uri $uri/ /index.php?$query_string;

3. 将 .php 文件传递给 PHP-FPM

最后,php_fastcgi 指令将 .php 文件路由到指定的 FPM 服务器地址。Caddy 知道如何正确设置 FPM 参数、拆分路径,并以深思熟虑的默认值执行其他几个“交接任务”。

php_fastcgi 指令是多个配置选项的简写。可以覆盖某些参数,或者如果不适合特定用例,可使用扩展形式进行细粒度配置。

在不进行重写的情况下提供单个 PHP 文件

如果不需要或不希望将所有请求重写为根目录下的 index.php 文件,可以配置 Caddy 将所有 .php 文件传递给 PHP-FPM,而不进行重写:

route {
    # Add trailing slash for directory requests
    @canonicalPath {
        file {path}/index.php
        not path */
    }
    redir @canonicalPath {http.request.orig_uri.path}/ 308

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

    # Proxy PHP files to the FastCGI responder
    @phpFiles path *.php
    reverse_proxy @phpFiles <php-fpm_gateway> {
        transport fastcgi {
            split .php
        }
    }
}

此配置几乎与扩展形式相同,但不重写为基线 index.php 文件:

    # 如果请求的文件不存在,尝试索引文件
    @indexFiles file {
-       try_files {path} {path}/index.php index.php
+       try_files {path} {path}/index.php
        split_path .php
    }
    rewrite @indexFiles {file_match.relative}

性能优化

Caddy 内置了多项性能改进,并默认进行了精细调优。

例如,encode zstd gzip指令会使 Caddy 在浏览器通过请求头表明其支持时,使用 zstd或 gzip对响应进行编码。Caddy 默认还会发送 Vary: Accept-encoding 头,以便 CDN 和其他缓存知道不要根据 Accept-encoding 值分割缓存。正是这些细节让 Caddy 成为一个现代、有明确立场且拥有良好默认值的服务器。

此外,Caddy v2.7 默认启用了多项出色的性能和安全性功能,包括 HTTP/3 和 TLS 1.3 支持、OCSP stapling 支持(这样浏览器无需查询 OCSP 服务器即可检查证书有效性)、自动 Alt-Svc 头、双 RSA+ECC 证书等。

使用 PHP Curl 扩展发起 HTTP/3 请求 如何使用 PHP Curl 扩展发起 HTTP/3 HTTP 请求,以及如何为 PHP 编译支持 HTTP/3 的 Curl。

对于 PHP,如果适用,还可以使用一些额外的性能优化:

快速 404 页面

与其将所有请求重写为 index.php 文件,有时更明智的做法是:如果请求 URI 指向 PHP 应用程序不处理的静态文件,则直接短路并立即结束响应。

以下是一个示例片段,它匹配针对特定扩展名(如 .jpg、.png、.woff2 等)的传入请求,并在该文件不存在时立即返回“未找到”响应。这避免了不必要地调用 PHP(可能成本更高,且通常也需要数据库连接)来从应用程序生成“未找到”错误。

@static_404 {  
  path_regexp \.(jpg|jpeg|png|webp|gif|avif|ico|svg|css|js|gz|eot|ttf|otf|woff|woff2|pdf)$  
  not file  
}  

respond @static_404 "Not Found" 404 {  
  close  
}

缓存头

Apache Web 服务器带有一个名为 Expires (mod_expires) 的模块,提供多个指令以轻松提供 Cache-Control 和 Expires 头。Expires 头已过时,因此由 Cache-Control 头决定浏览器用于控制其请求缓存的头。

虽然 Caddy 没有为此提供专用模块或一组指令,但可以使用现有的 header 指令来发送 Cache-Control 头:

@static {  
  path_regexp \.(jpg|jpeg|png|webp|gif|avif|ico|svg|css|js|gz|eot|ttf|otf|woff|woff2|pdf)$  
}  
header @static Cache-Control "max-age=31536000,public,immutable"

此片段向所有以 jpg/jpeg/png 等结尾的请求或其他静态文件类型发送 Cache-Control: max-age=31536000,public,immutable。结合快速 404,这些优化可以让 Caddy 快速高效地提供静态文件,同时利用浏览器/CDN 缓存。

match 指令 Caddy 的 match 指令可用于基于响应头设置头。

安全优化

Caddy 的主要特性之一是支持自动 HTTPS。这包括从 Let'sEncrypt 和 ZeroSSL 等证书颁发机构获取有效证书(使用 ACME 协议),以及自动 HTTPS 重定向。

它还为一组高度平衡且安全的 TLS 交换曲线和密码套件设置了配置值,同时确保默认配置始终安全。

如果需要以其他方式获取、验证和更新证书,可以关闭自动 TLS 证书和重定向。由于 Caddy 始终配置为具有合理且安全的默认值,PHP.Watch 不建议更改默认的 TLS/HTTPS 相关配置选项。

安全头

网站可以实施的最简单且最有效的安全优化之一是在应用程序中发送额外的安全头。这范围可以从强大且细粒度的头(如 CSP 和 Permissions-Policy)到头如 HSTS 和 X-Content-Type-Options。

以下显示了一组由 Caddy 配置文件设置的有明确立场的安全头。下面的示例很可能不适用于任何真实网站,但仅作为起点:

header {  
  Strict-Transport-Security "max-age=31536000;includeSubDomains;preload"  
  X-Frame-Options "SAMEORIGIN"  
  X-Xss-Protection "1;mode=block"  
  Referrer-Policy "no-referrer-when-downgrade"  
  X-Content-Type-Options "nosniff"
  Permissions-Policy "autoplay=(self),camera=(),geolocation=(),microphone=(),payment=(),usb=()"
  ?Content-Security-Policy "default-src 'self';script-src 'self';style-src 'self'"
}

Caddy 支持头前缀,如 ?,告诉 Caddy 仅在头未设置时发送该头,或使用 - 删除存在的头。

Cookie SameSite 标志

利用 Caddy 强大的头修改功能,可以通过编辑 Set-Cookie 头来提高从应用程序设置的 HTTP Cookie 的安全性:

header >Set-Cookie (.*) "$1; SameSite=Lax;"

注意,对于使用 PHP 会话的 PHP 应用程序,建议使用 PHP INI 内置设置来处理 SameSite Cookie。

限制请求方法

如果 PHP 应用程序未设计用于处理某些 HTTP 请求方法,或者根本不需要处理某些 HTTP 方法,则可以允许列出 HTTP 请求方法,使 Caddy 拒绝所有其他请求类型:

@requestMethodsList {  
    not method GET HEAD POST OPTIONS
}  
respond @requestMethodsList "Not Allowed" 405 {  
    close  
}

这类似于 Apache 的 AllowMethods 指令:

<Location />
  AllowMethods GET HEAD POST OPTIONS
</Location>

... 以及 Nginx 的 limit_except:

limit_except GET HEAD POST OPTIONS { deny  all; }

生产就绪

一些额外的优化可以帮助 PHP,并使 Caddy 指令与 PHP 应用程序保持一致。

受信任的代理

当 Caddy 位于代理、负载均衡器或 CDN 之后时,从 PHP 应用程序和 Caddy 本身观察到的客户端 IP 地址将设置为上一层的 IP 地址,而不是真实的客户端。

解决方法是设置代理/负载均衡器/CDN 的静态 IP 地址,使 Caddy 验证源 IP 地址是否在受信任的代理列表中,并使用来自(可配置的)X-Forwarded-For 头的客户端 IP 地址。

例如,如果有一个 IP 地址为 192.168.1.16 的负载均衡器:

trusted_proxies static 192.168.1.16

由于 Caddy 负责实际验证,PHP 应用程序可以依赖客户端 IP 地址而无需再次验证。

请求体大小和 PHP 上传限制 {#request-body}

Caddy 支持选择性地强制执行 HTTP 请求的最大体大小限制。如果此值小于 PHP 的 post_max_size(后者又覆盖了 upload_max_filesize),Caddy 将在请求传递给 PHP 之前终止请求。

request_body {
  max_size 20MB
}

设置 request_body max_size 有助于缓解 PHP 在请求超过 post_max_size 时有时出现的不可预测行为。

模块化且可重用的配置

当一个 Caddy 实例服务于多个网站,且在其它地方也合理时,Caddy 不仅支持导入配置文件(如上所示的主 Caddyfile 示例),还支持导入单个部分。

/etc/caddy/config/php.conf

(php83) {  
  php_fastcgi unix//run/php/php8.3-fpm.sock  
}
(php82) {  
  php_fastcgi unix//run/php/php8.2-fpm.sock  
}

各个部分(本例中的 (php83) 和 (php82))现在可以在任何其他配置文件中使用:

/etc/caddy/sites/example.com.conf

example.com {

    root * /var/www/example.com/public
    import php83

    # ...
}

总结

Caddy 是一个现代 Web 服务器,具有合理、快速且安全的默认配置。它开箱即用支持 HTTP/3 和 TLS 1.3、自动 HTTPS 和证书生命周期管理,并与 PHP 集成良好。

Caddy 可以使用最常见的“前端控制器”重写方式与 PHP 集成,或通过 PHP-FPM 提供单个 PHP 文件。

当配合额外的性能优化和安全优化进行微调时,Caddy 可以动态和静态地提供内容,并附加安全与缓存头、快速 404 页面以及其他最佳实践。此外,Caddy 中的请求限制和可信代理可配置为与 PHP 应用相匹配,以缓解 PHP 的一些不可预测行为,同时简化某些任务,例如强制实施可信代理 IP 地址。