Caddyfile 学习总结

tada-zako

前言

这几天由于想要更换远程服务器,同时重新整理一下服务器中服务的配置(原本由于经验不足,导致 docker 容器和直接运行的服务混在一起,实在不方便,特别是使用 docker 运行了 Web 服务器,由于容器网络隔离的原因,每次修改都是麻烦十足),所以又重新学习了一下关于 caddy 的相关知识。

Caddyfile 语法

虽然说是学习了 caddy 的相关知识,但其实学到的还是为了部署简单 web 服务的相关知识而已,基本上还就是部署静态网页、简单的反向代理简单服务罢了。毕竟没有涉及到真正对配置要求极高的生产环境…

基本语法

Caddyfile 的基本语法不算复杂,同时鉴于我个人使用的场景比较有限,这里直接以一个简单的例子来说明:

1
2
3
4
5
6
# Caddyfile
example.top {
root * /path/to/your/site
encode gzip zstd
file_server
}

如上,这就是一个可用的 Caddyfile 配置文件。它的含义是:当访问 example.top 这个域名时,Caddy 会将请求映射到 /path/to/your/site 目录下,同时启用静态文件服务器,并且启用 gzip 和 zstd 压缩静态文件,以提高传输效率。

如果想要反向代理到内部某个端口下的服务,可以这样写:

1
2
3
4
# Caddyfile
example.top {
reverse_proxy localhost:3000
}

如上,这个配置文件的含义是:当访问 example.top 这个域名时,Caddy 会将请求反向代理到 localhost:3000 这个地址下。

handle/route

关于 handleroute 的区别,这里特别推荐阅读 Caddy 社区提供的文章:Caddyfile Community

说到 Caddyfile 中的语法,最常见无疑是 handle 了。

handle 的作用是为了在相同嵌套层级中,从其他 handle 块中互斥地评估一组指令。

毫无疑问,上述的解释并不能让人直观的理解 handle 的作用,什么是“互斥地评估”?一连串的术语组成的句子无疑令人费解…我们先放下 Caddy 文档中的解释,先来看看 handle 的作用。

在上述提及到的 Caddy 社区文章中,作者举了两个关键的示例来引出 handle 的作用:

程序处理顺序

在 Caddyfile 中,指令的处理顺序是从上到下的,这一点和大多数配置文件类似。但是如果后一个指令是前一个指令的子集呢?例如:

1
2
reverse_proxy localhost:4000
reverse_proxy /api/* localhost:3000

如果,Caddyfile 中的指令顺序完全是线性的,那么上面的配置文件就会导致 /api/* 这个路径永远不会被匹配到,因为它已经被上面的 reverse_proxy localhost:4000 指令匹配到了。

为了解决上述问题,Caddyfile 会自动在内部对指令进行重新排序,确保更具体的路径匹配在前面。处理后的顺序如下:

1
2
reverse_proxy /api/* localhost:3000
reverse_proxy localhost:4000

互斥

像上述例子中的 reverse_proxy 指令其实是一个 handler 处理器,它会在处理请求时进行路径匹配,并且一旦匹配成功,就不会继续处理后续的 handler 了。

但是对于某些其它的指令呢?例如适配器 rewrite

1
2
3
rewrite /docs/json/*    /docs/json/index.html
rewrite /docs/modules/* /docs/modules/index.html
rewrite /docs/* /docs/index.html

上述的 rewrite 指令其实是互斥的,因为它们都是对路径进行重写的操作,并且一旦匹配成功,就不会继续处理后续的 rewrite 了。

但是如果 rewrite 不互斥呢,那么我们访问 /docs/json/somefile 这个路径时,就会先被第一个 rewrite 指令匹配到,然后重写为 /docs/json/index.html,接着又会被第三个 rewrite 指令匹配到,然后重写为 /docs/index.html,最终导致路径被错误地重写了。

因此,Caddyfile 目前有三个标准指令会与自身的其它实例互斥,它们分别是:

  • handlehandle_path
  • rewrite
  • root

问题

但是,有没有指令是不希望互斥的呢?答案是有的,例如 header 指令:

1
2
3
@options method   OPTIONS
header @options Access-Control-Allow-Methods "POST, GET, OPTIONS"
header Access-Control-Allow-Origin example.com

在这里,我们希望所有请求都能够被添加 Access-Control-Allow-Origin 头部,但是只有 OPTIONS 方法的请求才会被添加 Access-Control-Allow-Methods 头部。

出于上述意愿,Caddyfile 无法认为所有的 header 指令都是互斥的,因此 header 指令并不会与自身的其它实例互斥。

请看下面的例子:

1
2
header                Cache-Control max-age=86400
header /docs/foo.html Cache-Control no-cache

在这里,我们希望所有请求都能够被添加 Cache-Control: max-age=86400 头部,但是对于 /docs/foo.html 这个路径的请求,我们希望它能够被添加 Cache-Control: no-cache 头部。

看起来如果 Caddyfile 是完全从上到下处理指令的话,似乎 /docs/foo.html 这个路径的请求能够正确被添加 Cache-Control: no-cache 头部。但是实际上并不是这样的。

还记得 Caddyfile 会自动对指令进行重新排序吗?上述的两个 header 指令会被重新排序为:

1
2
header /docs/foo.html Cache-Control no-cache
header Cache-Control max-age=86400

所以,最终 /docs/foo.html 这个路径的请求会被添加 Cache-Control: max-age=86400 头部,而不是我们期望的 Cache-Control: no-cache 头部。

解决方法

使用 handle

还记得 handle 的作用吗?它的作用是为了在相同嵌套层级中,从其他 handle 块中互斥地评估一组指令。

因此,我们可以使用 handle 来将互斥的指令分组,从而避免它们被重新排序。

1
2
3
4
5
6
handle /docs/foo.html {
header Cache-Control no-cache
}
handle {
header Cache-Control max-age=86400
}

如上,这样就能够确保 /docs/foo.html 这个路径的请求能够正确被添加 Cache-Control: no-cache 头部,而不是被添加 Cache-Control: max-age=86400 头部。

使用 route

还记得我们这里是想说明什么来着,handleroute 的区别吗?

这里我们再来使用 route 来实现上述的需求:

1
2
3
4
route {
header Cache-Control max-age=86400
header /docs/foo.html Cache-Control no-cache
}

route 指令定义了一个 Caddyfile 无法重新排序的指令块,并且它与其它 route 块不互斥 (这很关键,route 并不互斥呦!)。使用 route 指令块中的指令会按照它们在块中的顺序依次处理。这样就能够确保 /docs/foo.html 这个路径的请求能够正确被添加 Cache-Control: no-cache 头部,而不是被添加 Cache-Control: max-age=86400 头部。

总结

综上,handleroute 的作用其实是类似的,都是为了避免指令的覆盖问题,只是两者的实现方式不同。 handle 是通过互斥的方式来实现的,而 route 是通过禁用重新排序来实现的。

这只是 Caddyfile 中不同的两种编写范式罢了, 并没有孰优孰劣之分,具体使用哪种方式,完全取决于个人喜好。

rewrite/redir/reverse_proxy

接下来,我们来看看 rewriteredirreverse_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。
rewritereverse_proxy 虽然看起来有些相似,但它们的作用是不同的。rewrite 是在服务器端重写请求的路径,而 reverse_proxy 是将请求代理到另一个服务器。

例如:

1
2
3
4
5
6
7
:2024 {
respond "Hello, 2024!" 200
}

:2025 {
rewrite localhost:2024
}

如上,这个配置文件的含义是:当请求路径为 localhost:2025 时,Caddy 会将请求路径重写为 localhost:2024,但是请求的其他部分不会改变。这是我们的期望,可实际上,由于 rewrite 指令只能修改请求的路径,而不能修改请求的服务器、端口号或协议,所以这个配置文件实际上是无效的。

但是如果我们使用 reverse_proxy 指令,上述的配置文件就能够正常工作:

1
2
3
4
5
6
:2024 {
respond "Hello, 2024!" 200
}
:2025 {
reverse_proxy localhost:2024
}

访问 localhost:2025 时,能够正确返回 Hello, 2024!

@matchers

@matchers 是 Caddyfile 中用于定义命名匹配器的语法。它允许你为一组条件创建一个命名的匹配器,然后在其他指令中引用该匹配器,从而简化配置文件的编写。

简单理解,就是提供了一种 「别名」 机制,允许你为一组条件创建一个命名的匹配器,然后在其他指令中引用该匹配器,从而简化配置文件的编写:

1
2
3
4
5
@my-matcher path /example/*

handle @my-matcher {
respond "This is an example response for GET requests to /example/*" 200
}

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
2
3
4
5
6
handle_errors {
@404 {
expression {http.error.status_code} == 404
}
respond @404 "Custom 404 Not Found" 404
}

这里使用 handle_errors 指令定义了一个错误处理器,当请求处理过程中发生 404 错误时,Caddy 会返回一个自定义的 404 响应。

basic_auth

basic_auth 指令用于启用基本认证,它会要求客户端提供用户名和密码才能访问受保护的资源。

例如:

1
2
3
4
5
6
example.com {
basic_auth {
admin admin
}
respond "Welcome, {http.auth.user.id}" 200
}

这样,当我们访问 example.com 时,浏览器会弹出一个认证对话框,要求我们输入用户名和密码。只有输入正确的用户名和密码后,才能访问受保护的资源。

同时,Caddyfile 推荐我们使用哈希值来存储密码,而不是明文密码。可以首先使用 caddy hash-password 命令来生成密码的哈希值,然后将哈希值放入 basic_auth 指令中:

1
2
3
4
5
6
example.com {
basic_auth {
admin $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
}
respond "Welcome, {http.auth.user.id}" 200
}

这里的 $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.
Comments