编程

如何将 Caddy Server 与 PHP 结合使用

2206 2023-11-14 17:10:00

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

2020 年 5 月发布的当前版本 Caddy 2 对其配置语法、自动化、插件等进行了重大改进。

本文介绍了如何将 PHP与 Caddy web 服务器版本2系列集成,以及高级配置。它还将类似的配置与 Apache 和 Nginx 配置进行了比较,以简化从 Apache 和 Nginx 到 Caddy 的迁移。

服务器初始安装

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

安装完 Caddy 后,就可以使用最小配置来配置 Caddy,如果存在静态文件,则可以为其提供服务,并将其他请求传递给 PHP-FPM。

example.com 本文示例中使用的域名,其源码位于  /var/www/example.com,其中的 /var/www/example.com/public/index.php 是网络应用的入口。该应用所有静态资源保存在 /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 请求日志),同时从 configsites 目录中加载另外的配置文件。

运行、开启、停止及重载 Caddy Server

如果 Caddy 安装成 systemd 服务,systemctl 命令可用于运行、开启、停止及重载 Caddy 服务器。

对于 ad-hoc 配置,服务器可以使用 caddy 命令进行控制:

systemctl start caddy
caddy start
caddy run # Starts server and blocks indefinitely

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/derivatives 和 RHEL/Fedora/derivativals 上,它几乎总是作为 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 域名 socket 不可用,请使用 IP 地址或者端口名。

PHP-FPM 监听的 Unix 域名 socket 或者 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 指令是多个配置选项的快捷方式。它可以重写特定的参数,或者如果不适用于特定用例时,可以使用 Expanded Form 进行颗粒度配置。

使用重定向服务单独的 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 文件:

    # 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 {file_match.relative}

性能微调

Caddy 内置了一些性能改进,并在默认情况下进行了微调。

例如,如果浏览器在其请求标头中指示浏览器可以处理响应,encode zstd zip 指令会使 Caddy 将响应编码为 zstdgzip。Caddy 默认情况下还会发送 Vary:Accept-encoding 头,因此 CDN 和其他缓存知道不要通过 Accept-encoding 对缓存进行分段。正是这样的小事让 Caddy 成为了一个现代化的、可选的服务器,具有良好的默认值。

此外,Caddy v2.7 默认启用了一些出色的性能和安全功能,包括 HTTP/3 和 TLS 1.3 支持、OCSP 装订支持(因此浏览器不必查询 OCSP 服务器来检查证书有效性)、自动 Alt-Svc 标头、双 RSA+ECC 证书等。

使用 PHP Curl 扩展发送 HTTP/3 请求
How to make HTTP/3 HTTP requests using PHP Curl extension, along with how to compile Curl with HTTP/3 support for PHP.

对于 PHP,可以进行一些额外的微调:

快速 404 页面

如果请求 URI 是 php 应用程序不处理的静态文件,那么有时缩短并立即结束响应是有意义的,而不是将所有请求重写到 index.php 文件。

以下是一个示例片段,它匹配某些扩展名(如 .jpg.png.woff2 等)的传入请求,如果不存在此类文件,则立即返回一个未找到页面的响应。这可以防止不必要地调用 PHP(可能更昂贵,而且通常还需要数据库连接),结果却只是在应用中生成 page-not-found 错误。

@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  
}

Cache Header

Apache web 服务器带有一个名为 Expires (mod_expires) 的模块,它提供多个指令来提供 Cache-ControlExpires 标头。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"

上面代码片段将发送 Cache-Control: max-age=31536000,public,immutable 到所有以 jpg/jpeg/png 等结尾或者其他静态文件类型的请求中。与 Fast 404 一起,这些微调将使 Caddy 在使用浏览器/CDN 缓存的同时,更快、更高效提供的静态文件服务。

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

安全微调

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

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

如果希望通过其他方式获取、验证和续订证书,则可以关闭自动 TLS 证书并重定向。由于 Caddy 总是使用合理和安全的默认值进行配置,因此不建议更改默认的 TLS/HTTPS 相关配置选项。

Security Headers

网站可以进行的最简单、最有效的安全调整之一是在应用程序中发送额外的安全标头。这可以是功能强大且细粒度的标头(如 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 ,只有在该 header 还未设置时发送该 header,或者 - 如果存在标头时删除。 

Cookie SameSite 标志

使用 Caddy 强大的 header 修改特性,可以改进由应用设置的 HTTP cookie 的安全性:

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

请注意,PHP 应用使用 PHP session,建议使用内置的 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 地址,而无需再次验证。

请求 Body 大小及 PHP 上传限制 {#request-body}

Caddy 支持选择性地强制执行 HTTP 请求的最大正文大小限制。如果这个值小于 PHP 的 post_max_size (这反过来又掩盖了upload_max_filesize),Caddy 将在将请求交给 PHP 之前终止请求。

request_body {
  max_size 20MB
}

设置 request_body max_size 可以帮助减少请求超过 post_max_size 时,PHP 偶发的预期外行为。

模块化和可复用配置

当单个 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 可以通过额外的安全和缓存标头、Fast 404 页面和其他最佳实践来提供动态和静态内容。此外,Caddy 中的请求限制和可信代理可以配置为与 PHP 应用程序相匹配,以缓解 PHP 的一些不可预测的行为,并简化某些任务,如强制执行可信代理 IP 地址。