Caddyfile 学习总结
前言
这几天由于想要更换远程服务器,同时重新整理一下服务器中服务的配置(原本由于经验不足,导致 docker 容器和直接运行的服务混在一起,实在不方便,特别是使用 docker 运行了 Web 服务器,由于容器网络隔离的原因,每次修改都是麻烦十足),所以又重新学习了一下关于 caddy 的相关知识。
Caddyfile 语法
虽然说是学习了 caddy 的相关知识,但其实学到的还是为了部署简单 web 服务的相关知识而已,基本上还就是部署静态网页、简单的反向代理简单服务罢了。毕竟没有涉及到真正对配置要求极高的生产环境…
基本语法
Caddyfile 的基本语法不算复杂,同时鉴于我个人使用的场景比较有限,这里直接以一个简单的例子来说明:
1 | # Caddyfile |
如上,这就是一个可用的 Caddyfile 配置文件。它的含义是:当访问 example.top 这个域名时,Caddy 会将请求映射到 /path/to/your/site 目录下,同时启用静态文件服务器,并且启用 gzip 和 zstd 压缩静态文件,以提高传输效率。
如果想要反向代理到内部某个端口下的服务,可以这样写:
1 | # Caddyfile |
如上,这个配置文件的含义是:当访问 example.top 这个域名时,Caddy 会将请求反向代理到 localhost:3000 这个地址下。
handle/route
关于
handle和route的区别,这里特别推荐阅读 Caddy 社区提供的文章:Caddyfile Community
说到 Caddyfile 中的语法,最常见无疑是 handle 了。
handle的作用是为了在相同嵌套层级中,从其他handle块中互斥地评估一组指令。
毫无疑问,上述的解释并不能让人直观的理解 handle 的作用,什么是“互斥地评估”?一连串的术语组成的句子无疑令人费解…我们先放下 Caddy 文档中的解释,先来看看 handle 的作用。
在上述提及到的 Caddy 社区文章中,作者举了两个关键的示例来引出 handle 的作用:
程序处理顺序
在 Caddyfile 中,指令的处理顺序是从上到下的,这一点和大多数配置文件类似。但是如果后一个指令是前一个指令的子集呢?例如:
1 | reverse_proxy localhost:4000 |
如果,Caddyfile 中的指令顺序完全是线性的,那么上面的配置文件就会导致 /api/* 这个路径永远不会被匹配到,因为它已经被上面的 reverse_proxy localhost:4000 指令匹配到了。
为了解决上述问题,Caddyfile 会自动在内部对指令进行重新排序,确保更具体的路径匹配在前面。处理后的顺序如下:
1 | reverse_proxy /api/* localhost:3000 |
互斥
像上述例子中的 reverse_proxy 指令其实是一个 handler 处理器,它会在处理请求时进行路径匹配,并且一旦匹配成功,就不会继续处理后续的 handler 了。
但是对于某些其它的指令呢?例如适配器 rewrite:
1 | rewrite /docs/json/* /docs/json/index.html |
上述的 rewrite 指令其实是互斥的,因为它们都是对路径进行重写的操作,并且一旦匹配成功,就不会继续处理后续的 rewrite 了。
但是如果 rewrite 不互斥呢,那么我们访问 /docs/json/somefile 这个路径时,就会先被第一个 rewrite 指令匹配到,然后重写为 /docs/json/index.html,接着又会被第三个 rewrite 指令匹配到,然后重写为 /docs/index.html,最终导致路径被错误地重写了。
因此,Caddyfile 目前有三个标准指令会与自身的其它实例互斥,它们分别是:
handle和handle_pathrewriteroot
问题
但是,有没有指令是不希望互斥的呢?答案是有的,例如 header 指令:
1 | @options method OPTIONS |
在这里,我们希望所有请求都能够被添加 Access-Control-Allow-Origin 头部,但是只有 OPTIONS 方法的请求才会被添加 Access-Control-Allow-Methods 头部。
出于上述意愿,Caddyfile 无法认为所有的 header 指令都是互斥的,因此 header 指令并不会与自身的其它实例互斥。
请看下面的例子:
1 | header Cache-Control max-age=86400 |
在这里,我们希望所有请求都能够被添加 Cache-Control: max-age=86400 头部,但是对于 /docs/foo.html 这个路径的请求,我们希望它能够被添加 Cache-Control: no-cache 头部。
看起来如果 Caddyfile 是完全从上到下处理指令的话,似乎 /docs/foo.html 这个路径的请求能够正确被添加 Cache-Control: no-cache 头部。但是实际上并不是这样的。
还记得 Caddyfile 会自动对指令进行重新排序吗?上述的两个 header 指令会被重新排序为:
1 | header /docs/foo.html Cache-Control no-cache |
所以,最终 /docs/foo.html 这个路径的请求会被添加 Cache-Control: max-age=86400 头部,而不是我们期望的 Cache-Control: no-cache 头部。
解决方法
使用 handle
还记得 handle 的作用吗?它的作用是为了在相同嵌套层级中,从其他 handle 块中互斥地评估一组指令。
因此,我们可以使用 handle 来将互斥的指令分组,从而避免它们被重新排序。
1 | handle /docs/foo.html { |
如上,这样就能够确保 /docs/foo.html 这个路径的请求能够正确被添加 Cache-Control: no-cache 头部,而不是被添加 Cache-Control: max-age=86400 头部。
使用 route
还记得我们这里是想说明什么来着,handle 和 route 的区别吗?
这里我们再来使用 route 来实现上述的需求:
1 | route { |
route 指令定义了一个 Caddyfile 无法重新排序的指令块,并且它与其它 route 块不互斥 (这很关键,route 并不互斥呦!)。使用 route 指令块中的指令会按照它们在块中的顺序依次处理。这样就能够确保 /docs/foo.html 这个路径的请求能够正确被添加 Cache-Control: no-cache 头部,而不是被添加 Cache-Control: max-age=86400 头部。
总结
综上,handle 和 route 的作用其实是类似的,都是为了避免指令的覆盖问题,只是两者的实现方式不同。 handle 是通过互斥的方式来实现的,而 route 是通过禁用重新排序来实现的。
这只是 Caddyfile 中不同的两种编写范式罢了, 并没有孰优孰劣之分,具体使用哪种方式,完全取决于个人喜好。
rewrite/redir/reverse_proxy
接下来,我们来看看 rewrite、redir 和 reverse_proxy 这三个指令。这里主要想说明的是它们的区别。
rewrite
rewrite 指令用于重写请求的路径,它不会改变请求的其他部分,例如查询参数和头部。它通常用于将请求路径映射到不同的资源。
例如:
1 | rewrite /old-path /new-path |
如上,这个配置文件的含义是:当请求路径为 /old-path 时,Caddy 会将请求路径重写为 /new-path,但是请求的其他部分不会改变。
redir
redir 指令用于重定向请求,它会改变请求的路径,并且会返回一个 3xx 状态码给客户端,告诉客户端去请求新的路径。它通常用于将请求重定向到不同的 URL。
例如:
1 | redir /old-path /new-path 301 |
如上,这个配置文件的含义是:当请求路径为 /old-path 时,Caddy 会返回一个 301 状态码,并且告诉客户端去请求 /new-path。
reverse_proxy
reverse_proxy 指令用于将请求反向代理到另一个服务器,它会将请求的路径、查询参数和头部都转发给目标服务器,并且会将目标服务器的响应返回给客户端。它通常用于将请求代理到后端服务。
例如:
1 | reverse_proxy /api/* localhost:3000 |
如上,这个配置文件的含义是:当请求路径以 /api/ 开头时,Caddy 会将请求反向代理到 localhost:3000 这个地址下,并且会将请求的路径、查询参数和头部都转发给目标服务器。
注意点
redir 是 Web 服务器提供的一种重定向机制,告诉客户端去请求新的 URL。rewrite 和 reverse_proxy 虽然看起来有些相似,但它们的作用是不同的。rewrite 是在服务器端重写请求的路径,而 reverse_proxy 是将请求代理到另一个服务器。
例如:
1 | :2024 { |
如上,这个配置文件的含义是:当请求路径为 localhost:2025 时,Caddy 会将请求路径重写为 localhost:2024,但是请求的其他部分不会改变。这是我们的期望,可实际上,由于 rewrite 指令只能修改请求的路径,而不能修改请求的服务器、端口号或协议,所以这个配置文件实际上是无效的。
但是如果我们使用 reverse_proxy 指令,上述的配置文件就能够正常工作:
1 | :2024 { |
访问 localhost:2025 时,能够正确返回 Hello, 2024!。
@matchers
@matchers 是 Caddyfile 中用于定义命名匹配器的语法。它允许你为一组条件创建一个命名的匹配器,然后在其他指令中引用该匹配器,从而简化配置文件的编写。
简单理解,就是提供了一种 「别名」 机制,允许你为一组条件创建一个命名的匹配器,然后在其他指令中引用该匹配器,从而简化配置文件的编写:
1 | @my-matcher path /example/* |
try_files
try_files 指令用于尝试按顺序查找一组文件,并返回第一个存在的文件。如果所有文件都不存在,则可以指定一个备用响应。
例如:
1 | try_files {path} /index.html |
当请求路径为 {path} 时,Caddy 会尝试查找该路径对应的文件,如果该文件不存在,则返回 /index.html 文件。
这条指令常用于 SPA(单页应用)中,因为 SPA 通常只有一个入口文件(例如 index.html),而其他路径都是通过前端路由来处理的。这时候,为了防止 Caddy 服务器将请求路径映射到不存在的文件上,我们可以使用 try_files 指令来确保所有请求都返回 index.html 文件,至于具体的请求路径,会交给前端路由来处理(包括404页面,但是需要前端路由有配置…)。
handle_errors
handle_errors 指令用于定义错误处理器,当请求处理过程中发生错误时,Caddy 会调用该错误处理器来生成响应。
简单示例如下:
1 | handle_errors { |
这里使用 handle_errors 指令定义了一个错误处理器,当请求处理过程中发生 404 错误时,Caddy 会返回一个自定义的 404 响应。
basic_auth
basic_auth 指令用于启用基本认证,它会要求客户端提供用户名和密码才能访问受保护的资源。
例如:
1 | example.com { |
这样,当我们访问 example.com 时,浏览器会弹出一个认证对话框,要求我们输入用户名和密码。只有输入正确的用户名和密码后,才能访问受保护的资源。
同时,Caddyfile 推荐我们使用哈希值来存储密码,而不是明文密码。可以首先使用 caddy hash-password 命令来生成密码的哈希值,然后将哈希值放入 basic_auth 指令中:
1 | example.com { |
这里的 $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG 包含多个部分,分别表示哈希算法、成本因子和盐值等信息。
- Title: Caddyfile 学习总结
- Author: tada-zako
- Created at : 2025-10-14 00:00:00
- Updated at : 2025-10-23 15:22:19
- Link: https://blog.tada-zako.top/2025/技术/caddy-study/
- License: This work is licensed under CC BY-NC-SA 4.0.