Caddyfile 语法浅析

发现大部分人在切换 Caddy 时遇到的比较大的困难就是这个 Caddyfile 不知道怎么写,一开始我也是很懵逼的状态,今天决定写写这个 Caddyfile 配置语法,顺便自己也完整的学学。

一、Caddy 配置体系

在 Caddy1 时代,Caddy 自创了一种被称之为 Caddyfile 的配置文件格式,当然可以理解为创造了一种语法,这里面深入的说就涉及到了编译原理相关知识,这里不再展开细谈(因为我也不会);Caddyfile 由内部的语法解析器进行语法、词法分析最后 “序列化” 到 Go 的配置结构体中。

随着 Caddy 壮大,到了 Caddy2 时代人们已经并不满足于单纯的 Caddyfile 配置,因为学习 Caddyfile 是有代价的,负载均衡器选型的切换本身就代价很大,还要去花心思学习 Caddyfile 语法,这无异非常痛苦。所以 Caddy2 在经过取舍过后决定使用 json 作为内部标准配置,然后其他类型的配置通过 Config Adapters 将其转换为 json 再使用,而 Caddyfile 的 Adapter 作为官方支持的内置 Adapter 存在。

最终要说明的是: Caddyfile 里支持哪些指令是由 Caddyfile 的 Adapter 决定的,内部的 json 配置对应的指令名称可能跟 Caddyfile 不同,也可能内部 json 支持一些指令,而 Caddyfile 根本不支持。

二、Caddyfile 基本结构

开局一张图,文章全靠编(下面是官方的语法结构图)

2.1、全局选项

在一个 Caddyfile 内(空白文本文件),如果仅以两个大括号括起来的配置就是全局配置项,例如下面的配置:

{
	debug
	http_port  8080
	https_port 8443
}

那么一共有哪些全局配置项呢?当然是看 官方文档:

{
	# General Options
	debug
	http_port  <port>
	https_port <port>
	order <dir1> first|last|[before|after <dir2>]
	storage <module_name> {
		<options...>
	}
	storage_clean_interval <duration>
	admin   off|<addr> {
		origins <origins...>
		enforce_origin
	}
	log [name] {
		output  <writer_module> ...
		format  <encoder_module> ...
		level   <level>
		include <namespaces...>
		exclude <namespaces...>
	}
	grace_period <duration>

	# TLS Options
	auto_https off|disable_redirects|ignore_loaded_certs
	email <yours>
	default_sni <name>
	local_certs
	skip_install_trust
	acme_ca <directory_url>
	acme_ca_root <pem_file>
	acme_eab <key_id> <mac_key>
	acme_dns <provider> ...
	on_demand_tls {
		ask      <endpoint>
		interval <duration>
		burst    <n>
	}
	key_type ed25519|p256|p384|rsa2048|rsa4096
	cert_issuer <name> ...
	ocsp_stapling off
	preferred_chains [smallest] {
		root_common_name <common_names...>
		any_common_name  <common_names...>
	}

	# Server Options
	servers [<listener_address>] {
		listener_wrappers {
			<listener_wrappers...>
		}
		timeouts {
			read_body   <duration>
			read_header <duration>
			write       <duration>
			idle        <duration>
		}
		max_header_size <size>
		protocol {
			allow_h2c
			experimental_http3
			strict_sni_host
		}
	}
}

这些全局配置具体都什么意思这里就不细说了,请自行查阅文档;当然文档也可能并不一定准确,有些兴趣的可以去查看 Caddy 源码,这些都在源码中定义了 caddyconfig/httpcaddyfile/options.go:28

2.2、代码块

叫代码块可能不太恰当,也可以叫做配置块或配置片段;这是 Caddyfile 比较棒的一个功能,配置片段可以实现类似代码这种引用使用,方便组合配置文件;配置片段的语法如下:

(配置片段名字) {
    # 这里写配置片段的内容
}

下面是一个配置片段示例(不能运行,只是举例):

# 定义一个叫 TLS_INTERMEDIATE 的配置片段
(TLS_INTERMEDIATE) {
    protocols tls1.2 tls1.3
    ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}

www.mritd.com {
    # 重定向
    redir https://mritd.com{uri}

    # 这里引用上面的 TLS_INTERMEDIATE 配置
    import TLS_INTERMEDIATE
}

这种写法与下面的配置等价,目的就是增加配置的重用和规范化:

www.mritd.com {
    # 重定向
    redir https://mritd.com{uri}

    protocols tls1.2 tls1.3
    ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}

2.3、站点配置

站点配置是 Caddyfile 的核心中的核心,从开局的图上也可以看到,能在 “Top Level” 上存在的只有三种配置,其中就包含了这个站点配置块,站点配置块格式如下:

站点域名 {
    # 其他配置
}

以下是两个合法的站点配置示例:

example1.com {
	root * /www/example.com
	file_server
}

example2.com {
	reverse_proxy localhost:9000
}

2.4、自定义匹配器

请求匹配器是 Caddy 内置的一种针对请求的过滤工具,有点类似于 nginx 配置中的 location /api {...},只不过 Caddyfile 中的匹配器更加强大;标准的请求匹配器列表如下:

自定义命名匹配器的作用是组合多个标准匹配器,然后实现复用,自定义命名匹配器语法如下:

# @ 后面跟一个自定义名称
@api {
    # 标准匹配器组合
    path /api/*
    host example.com
}

然后这个自定义的命名匹配器可以在其他位置引用:

example.com {
    @api {
        # 标准匹配器组合
        path /api/*
        host example.com
    }
    
    reverse_proxy @api 127.0.0.1:9000
}

三、Caddyfile 语法细节

3.1、Blocks

Caddyfile 中的配置块可以理解为代码中的作用域,其包含两个大括号范围内的所有配置:

... {
    ...
}

当 Caddyfile 中只有一个站点配置,且不需要其他全局配置等信息时,Blocks 可以被省略,例如:

example.com {
    reverse_proxy /api/* localhost:9001
}

这个配置可以直接简写为:

example.com

reverse_proxy /api/* localhost:9001

这么做的目的是方便单站点快速配置,但是一般不常用也不推荐使用。在同一个 Caddyfile 中可以包含多个站点配置,只要地址不同即可:

example.com {
    ...
}

abcd.com {
    ...
}

3.2、Directives

指令是指描述站点配置的一些关键字,例如下面的站点配置文件:

example.com {
    reverse_proxy /api/* localhost:9001
}

在这个配置文件中 reverse_proxy 就是一个指令,同时指令还可能包含子指令(Subdirectives),下面的配置中 lb_policy 就是 reverse_proxy 的一个子指令:

example.com {
    reverse_proxy localhost:9000 localhost:9001 {
        lb_policy first
    }
}

3.3、Tokens and quotes

在 Caddyfile 被 Caddy 读取后,Caddy 会将配置文件解析为一个个的 Token;Caddyfile 中所有 Token 都认为是空格分割,所以如果某些指令需要传递参数时我们需要通过合理的空格和引号来确保 Token 正确解析:

example.com {
    # 这里 localhost:9000 localhost:9001 空格分割就认为是两个 Token
    reverse_proxy localhost:9000 localhost:9001
}

如果某些参数需要包含空格,那么需要使用双引号包裹:

example.com {
    file_server {
        # 双引号包裹住有空格的参数
        root "/data/Application Data/html"
    }
}

如果这个参数里需要包含双引号,只需要通过反斜线转义即可,例如 "\"a b\"";如果有太多的双引号或者空格,可以使用 Go 语言中类似的反引号来定义 “绝对字符串”:

example.com {
    file_server {
        # 反引号包裹
        root `/data/Application Data/html`
    }
}

3.4、Addresses

Caddyfile 中的地址其实是一种很宽泛的格式,在上面讲站点配置时其实前面的字符串并不一定是域名,准确的说应该是地址:

地址 {
    # 站点具体配置
}

在 Caddyfile 中以下格式全部都是合法的地址:

  • localhost
  • example.com
  • :443
  • http://example.com
  • localhost:8080
  • 127.0.0.1
  • [::1]:2015
  • example.com/foo/*
  • *.example.com
  • http://

需要注意的是: 自动 HTTPS 是 Caddy 服务器的一个重要特性,但是自动 HTTPS 会隐式进行,除非在地址中明确的写明 http://example.com 这种格式时 Caddy 才会单纯监听 HTTP 协议,否则域名格式的地址 Caddy 都会进行 HTTPS 证书申请。

如果地址中指定了域名,那么只有匹配到域名的请求才会接受;例如地址为 localhost 的站点不会响应 127.0.0.1 方式的访问请求。同时地址中可以采用 * 作为通配符,通配符作用域仅在域名的英文句号 . 之内,意思就是说 *.example.com 会匹配 test.example.com 但不会匹配 abc.test.example.com

如果多个域名/地址共享一个站点配置,可以采用英文逗号分隔的方式写在一起:

example.com,www.example.com,localhost,127.0.0.1:8080 {
    file_server {
        root /data/html
    }
}

3.5、Matchers

匹配器其实在第一部分已经介绍过,这里仅做一下简单说明;匹配器一般紧跟在指令之后,其大致格式分为以下三种:

  • *: 匹配所有请求(通配符)
  • /path: 匹配特定路径
  • @name: 自定义命名匹配器

匹配器的用法样例如下:

# 自定义一个叫 websockets 的匹配器
@websockets {
    # 匹配请求头 Connection 中包含 Upgrade 的请求
    header Connection *Upgrade*
    
    # 匹配请求头 Upgrade 为 websocket 的请求
    header Upgrade    websocket
}

# 反向代理时使用 @websockets 匹配器
reverse_proxy @websockets localhost:6001

具体更细节的官方匹配器使用限于篇幅这里不再详细说明,请自行阅读 官方文档

3.6、Placeholders

占位符可以理解为 Caddyfile 内部的变量替换符号,占位符同样以大括号包裹,同时支持转义:

# 标准占位符
{system.hostname}

# 避免冲突可进行转义
\{system.hostname\}

Caddyfile 内部可用的占位符有很多,但是并非在所有情况下都可用,比如 HTTP 相关的占位符仅在处理 HTTP 请求相关配置中才可用;同时占位符也支持简写,下面是官方目前支持的占位符列表:

3.7、Snippets

片段上面也介绍过了,这里说一下片段更高级的用法: 支持参数传递;下面是定义一个通用日志格式,然后通过参数引用实现不同站点使用不同日志文件的配置:

(LOG_COMMON) {
    log {
        format formatted "[{ts}] {request>remote_addr} {request>proto} {request>method} <- {status} -> {request>host} {request>uri} {request>headers>User-Agent>[0]}"  {
            time_format "iso8601"
        }
        
        # {args.0} 声明引用传入的第一个参数
        output file "{args.0}" {
            roll_size 100mb
            roll_keep 3
            roll_keep_for 7d
        }
    }
}

example.com {
    # 此时 /data/log/example.com.log 作为 "{args.0}" 被传入
    import LOG_COMMON /data/log/example.com.log
}

3.8、Comments

注释没啥好说的,以 # 作为开头就行了。

3.9、Environment variables

环境变量和占位符类似,不同的是占位符是 Caddyfile 内置的变量,而环境变量是引用系统环境变量;环境变量的使用格式如下(推荐全大写):

# 引用一个叫 SITE_ADDRESS 的环境变量
{$SITE_ADDRESS} {
    # 站点具体配置...
}

上面的配置在 Caddy 启动时会读取 SITE_ADDRESS 作为监听地址,如果 SITE_ADDRESS 读取不到则会报错退出;如果想要为 SITE_ADDRESS 设置默认值,那么只需要使用如下格式即可:

{$SITE_ADDRESS:localhost} {
    # 站点具体配置...
}

四、其他补充

Caddyfile 并不是万能的,但是 Caddyfile 因为更易于编写和维护所以使用比较广泛;在第一部分介绍 Caddy 的配置文件体系时已经说明了,实际上 Caddy 内部是使用 json 作为配置的;这时就可能出现一些极端情况,比如说真的某个配置只能通过 json 配置,那么这时候可以考虑先通过 json 管理 API 进行动态修改,然后再去向官方发 issue,有能力也可以直接 PR;API 动态修改的流程如下:

首先假设你已经有一个能够正常启动的 Caddyfile,但是某个配置选项不支持,这时候你可以通过 API 获取内部的 json 配置:

# 结尾一定要有 /
➜ ~ curl localhost:2019/config/
{"apps":{"http":{"servers":{"srv0":{"listen":[":80"],"routes":[{"handle":[{"canonical_uris":false,"handler":"file_server","hide":["./Caddyfile"]}]}]}}}}}

得到这个配置以后,你可以通过格式化工具格式化 json,然后添加特定选项,再将其保存到一个配置文件中,然后重新 load 回去即可:

# 假设修改后的 json 文件叫 caddy.json
➜ ~ curl -XPOST http://localhost:2019/load -H "Content-Type: application/json" -d @caddy.json

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 国际许可协议进行许可,转载请注明出处。