Caddy 简介
官方给的简介是: Caddy是一个强大的、可扩展的平台, 用于伺服你的站点、服务以及应用. Caddy使用Go语言编写.
简单来说, caddy 和 nginx 很像, 我觉得相比较nginx有以下优点:
- 安装简单 caddy的是个单文件可执行程序, 没有任何依赖, 下载下来就可以使用. 对于简单的web服务, 使用命令行即可运行, 不需要任何配置文件.
- 自动签发HTTPS证书 caddy默认对所有站点开启HTTPS(支持Let's encrypt 和 ZeroSSL证书自动申请).
- 原生支持HTTP API 支持使用HTTP API方式修改配置.
缺点就是生态和性能不如nginx, 不过个人使用绝大部分场景都能hold住.
Caddy由ZeroSSL开源, 其原生配置语言是JSON, 但是手写JSON配置比较麻烦且易错, 所以Caddy支持配置适配器(config adapter), 适配器是通过插件形式实现. 除了官方建议使用的 Caddyfile 文件配置, 还提供了nginx配置适配. 通过 nginx-adapter, 可以让 caddy 支持nginx的配置文件.
基本命令
启动
# 在前台启动 caddy 服务, 按 ctrl + c 终止服务
caddy run --config ./Caddyfile --adapter caddyfile
# 在后台启动 caddy 服务
caddy start --config ./Caddyfile --adapter caddyfile
# 或者使用 docker
docker run -d \
  --name caddy-run \
  -p 80:80 \
  -p 443:443 \
  -v $PWD/Caddyfile /etc/caddy/Caddyfile \
  caddy
其他参数:
- --adapter用于指定配置适配器(更多适配器参考: config-adapters )
- --watch参数可以监视配置文件(由参数- --config指定)变化, 并自动重新加载服务
常用使用方式
caddy 支持CLI方式伺服静态文件和反向代理, 同时支持配置文件进行更复杂的服务伺服.
Hello world
假设配置文件如下(需要把demo.alanwei.com替换成你自己的域名或者localhost):
demo.alanwei.com
respond "Hello, privacy!"
执行命令 caddy run --config ./Caddyfile, 然后访问 https://demo.alanwei.com 就会返回 Hello, privacy!, HTTPS 支持完全自动化.
静态文件 static files
CLI方式
基本命令 caddy file-server [--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log]
示例:
# 指定端口号
caddy file-server --listen :2015 
# 也指定目录
caddy file-server --root $PWD/build --listen 0.0.0.0:3001
常用参数:
- --listen [<host>]:<port>指定监听的端口号,- <host>可以省略
- --browse如果站点没有index文件, 允许列出站点下文件目录
- --root <dir>默认站点目录是当前目录, 可通过改参数修改默认行为
- --access-log是否打印访问日志
Caddyfile方式
对应的 Caddyfile 基本配置如下:
localhost:2015
file_server
指令file_server常用选项:
http://files.alanwei.com {
	root * /data/software # 指定目录
	file_server browse {
		index "REAME.md" index.html index.txt # 设置首页文件
		hide android *.sh "**/node_modules/**" # 隐藏特定文件或目录, 支持占位符和glob patterns
	}
}
反向代理 reverse proxy
CLI方式
基本命令: caddy reverse-proxy
# 把所有 localhost 的请求都转发到 127.0.0.1:9000
caddy reverse-proxy --to 127.0.0.1:9000
# 把所有 *:3013 的请求都转发到 127.0.0.1:3010 , 并禁用https
caddy reverse-proxy --insecure -from :3013 -to 127.0.0.1:3010
支持参数:
- --change-host-header修改upstream的 Host
- --from [<host>]:<port>接收请求的地址, 默认是 localhost
- --insecure禁用 TLS 校验
- --to代理到的地址
Caddyfile
Structure
Caddyfile的文件结构如下:

- 文件开始的全局配置块(global options block)是可选的
- 如果没有全局配置块, Caddyfile 文件的第一行一定是需要伺服的站点的地址(address)
- 如果文件仅有一个站点配置块(site block), 那配置块的花括号({ })是可以省略的
Caddyfile 至少有一个站点配置块(site block), 如果指令(directive)出现在站点配置块的地址(address)前, 那配置文件解析将会失败.
blocks
使用花括号开启和关闭一个block:
... {
  ...
}
- 左花括号({)必须在行尾
- 右花括号(})必须独占一行
当配置文件仅有一个站点配置块(site block), 为了方便快速定义单个站点, 花括号(和block的内容缩进)是可选的, 比如:
localhost
reverse_proxy /api/* localhost:9001
file_server
等价于
localhost {
	reverse_proxy /api/* localhost:9001
	file_server
}
如果有多个站点, 必须使用花括号:
example1.com {
	root * /www/example.com
	file_server
}
example2.com {
	reverse_proxy localhost:9000
}
如果一个请求匹配到了多个块(site block), 则会选用最精确地址(address)的那个配置块(site block), 也就是说请求只会被一个block接管,不会级联传递到多个block.
Directives
指令(directive)关键词用于定义站点如何被伺服, 比如file_server表示静态文件站点, reverse_proxy表示反向代理站点.
在站点配置块里(site block), 指令必须是其所在行的第一个单词, 指令后面的单词是传给指令的参数
当Caddyfile被适配的时候, 指令的会按照一定规则进行排序, 参考directive order
子指令可以出现在指令块中:
localhost
reverse_proxy localhost:9000 localhost:9001 {
	lb_policy first
}
lb_policy(load balance policy) 是 reverse_proxy 的子指令, 用于设置有多个后端服务时的负载均衡.
Tokens and quotes
词法和引号, 解析Caddyfile的时候,通过空格拆分token, 比如directive abc def表示指令会收到两个参数, 而directive "abc def"指令就只收到一个参数, 可以使用以下方式传递带有引号的参数值:
directive "\"abc def\""
directive `"abc def"`
Addresses
站点的地址(address)必须出现在站点配置块(site block)的开头, 有效的地址格式如下:
- localhost
- example.com
- :443
- http://example.com
- localhost:8080
- 127.0.0.1
- [::1]:2015
- example.com/foo/*
- *.example.com
- http://
如果你指定了主机名/域名(hostname), 那只有请求头的Host和hostname匹配的请求才会被处理, 和hostname不匹配的请求则会被忽略. 比如站点配置的地址是localhost, 那caddy就不会匹配请求头Host: 127.0.0.1 的请求.
地址中可以使用通配符*, 但是通配符只会匹配一段, 比如*.example.com会匹配foo.example.com, 但是不会匹配foo.bar.example.com. 另外, 如果使用*作为整个地址, 会匹配localhost, 但是不会匹配 example.com.
如果多个地址(address)使用相同的配置定义, 可以把他们放在一起, 使用逗号分割:
localhost:8080, example.com, www.example.com {
}
# 或者如下
localhost:8080,
example.com,
www.example.com {
}
同名地址不能多次指定.
Matchers
Request matchers 用于使用指定的标准(specific criteria)来过滤或者界定(filter or classify)请求.
在Caddyfile文件中, 使用 matcher token(以下简称 匹配词) 限定指令的作用域, 可用的匹配词如下:
- *匹配所有的请求(默认行为)
- /path按照请求路径匹配
- @name使用命名匹配(named matcher)
匹配词(matcher token)通常是可选的, 如果省略匹配词等同于使用通配符匹配词: *.
简单示例如下:
root *           /var/www  # matcher token: *
root /index.html /var/www  # matcher token: /index.html
root @post       /var/www  # matcher token: @post
reverse_proxy /api/* localhost:9000 # 匹配所有 /api/ 的请求路径
如果想匹配所有请求, * 可以省略, 比如reverse_proxy localhost:9000等价于reverse_proxy * localhost:9000.
如果想使用多个匹配条件限制请求, 可以定义命名匹配(named matcher), 然后通过@name形式引用命名匹配:
@postfoo {
	method POST
	path /foo/*
}
reverse_proxy @postfoo localhost:9000
Path matchers
路径匹配词(path matcher token)必须以 / 开始, 默认请求下路径匹配模式是完全匹配, 可以使用通配符进行模糊匹配, 比如 /foo* 可以同时匹配以下路径:
- /foo
- /foo/
- /foobar
Named matchers
所有非路径和通配符匹配的, 都必须使用命名匹配(named matcher):
@websockets {
	header Connection *Upgrade*
	header Upgrade    websocket
}
reverse_proxy @websockets localhost:6001
上面的命名匹配则表示仅匹配请求头Connection包含Upgrade, 且请求头Upgrade包含websocket的请求.
如果命名匹配器仅有一条匹配规则, 可以将name和规则写在一行:
@post method POST
reverse_proxy @post localhost:6001
命名匹配定义了匹配规则集, 定义的规则使用AND组合, 也就是说请求必须满足所有规则才算通过命名匹配.
Placehodlers
为了便于在静态配置文件Caddyfile中使用变量, Caddy 支持以下占位符(placeholder):
| 简写 | 全称 | 
|---|---|
| {dir} | {http.request.uri.path.dir} | 
| {file} | {http.request.uri.path.file} | 
| {header.*} | {http.request.header.*} | 
| {host} | {http.request.host} | 
| {labels.*} | {http.request.host.labels.*} | 
| {hostport} | {http.request.hostport} | 
| {port} | {http.request.port} | 
| {method} | {http.request.method} | 
| {path} | {http.request.uri.path} | 
| {path.*} | {http.request.uri.path.*} | 
| {query} | {http.request.uri.query} | 
| {query.*} | {http.request.uri.query.*} | 
| {re.*.*} | {http.regexp.*.*} | 
| {remote} | {http.request.remote} | 
| {remote_host} | {http.request.remote.host} | 
| {remote_port} | {http.request.remote.port} | 
| {scheme} | {http.request.scheme} | 
| {uri} | {http.request.uri} | 
| {tls_cipher} | {http.request.tls.cipher_suite} | 
| {tls_version} | {http.request.tls.version} | 
| {tls_client_fingerprint} | {http.request.tls.client.fingerprint} | 
| {tls_client_issuer} | {http.request.tls.client.issuer} | 
| {tls_client_serial} | {http.request.tls.client.serial} | 
| {tls_client_subject} | {http.request.tls.client.subject} | 
| {tls_client_certificate_pem} | {http.request.tls.client.certificate_pem} | 
| {tls_client_certificate_der_base64} | {http.request.tls.client.certificate_der_base64} | 
| {upstream_hostport} | {http.reverse_proxy.upstream.hostport} | 
Snippets
可以在Caddyfile中定义片段, 方便在其他block中复用, 必须下面定义一个名为 snippet 的片段, 然后在另外另个block中使用import snippet:
(snippet) {
  respond "Yahaha! You found {args.0}!"
}
a.example.com {
	import snippet "Example A"
}
b.example.com {
	import snippet "Example B"
}
Comments
Caddyfile中#后面的内容视为注释.
Automatic HTTPS
Overview
默认情况下, Caddy使用HTTPS伺服所有站点
- 对于地址为IP、本地地址或者内网地址的站点, 比如 localhost,127.0.0.1等, Caddy使用自签名证书伺服.(使用命令caddy trust手动信任证书)
- 对于公开的DND域名, 比如example.com,sub.example.com或者*.example.com, Caddy使用公开的 ACME CA 证书, 比如 Let's Encrypt 或者 ZeroSSL.
Caddy自动为站点续签证书, 并默认把HTTP请求跳转到HTTPS.
Activation
Caddy隐式自动为站点提供HTTPS服务,有多种方式设置Caddy站点的伺服的域名或者IP:
- Caddyfile 的 site address
- route 的 host matcher
- 命令行的参数, 比如 --domain 或者 --from
- 证书加载器: automate
下面是禁用Caddy默认HTTPS伺服行为的方法:
- 显式禁用
- 在配置文件中不提供任何主机名或IP
- 地址使用非HTTP端口号(80, 443)
- 地址协议使用 http
- 手动加载证书
在Caddyfile配置文件的全局配置里增加auto_https off即可显式禁用caddy所有HTTPS伺服行为.
Local HTTPS
为了通过HTTPS伺服非公开站点(即本地站点), Caddy使用自签名证书, 签名证书存储在 Caddy's data directory 的 pki/authorities/local 目录下.
Caddy的数据目录(data directory)默认路径为:
| OS | Data directory path | 
|---|---|
| Linux, BSD | $HOME/.local/share/caddy | 
| Windows | %AppData%\Caddy | 
| macOS | $HOME/Library/Application Support/Caddy | 
| Plan 9 | $HOME/lib/caddy | 
| Android | $HOME/caddy(or/sdcard/caddy) | 
如果存在环境变量XDG_DATA_HOME, 优先使用环境变量指定的目录.
常用指令
指令都是在site block内使用
log
语法
log {
	output <writer_module> ...
	format <encoder_module> ...
	level  <level>
}
示例
books.alanwei.com {
	reverse_proxy localhost:8088
	log {
		output file books.alanwei.com.access.log {
			roll_size 10MiB # 设置文件大小
			roll_keep 10 # 设置文件保留数量
			roll_keep_for 48h # 设置日志保留时长
		}
		format console # 还支持 json
		level INFO
	}
}
basicauth
HTTP Basic Authentication, 设置HTTP基本认证功能.
Caddy配置文件里的密码必须hash计算之后的, 可以使用 caddy hash-password 命令计算密码的hash.
用户访问认证通过后, {http.auth.user.id} 占位符就变成可用了, 其值是用户名.
语法
basicauth [<matcher>] [<hash_algorithm> [<realm>]] {
	<username> <hashed_password_base64> [<salt_base64>]
	...
}
- <hash_algorithm>密码的hash算法, 可以是- bcrypt(默认)或者- scrypt
- <realm>is a custom realm name.
- <username>用户名或者用户Id
- <hashed_password_base64>hash密码经过base64编码的值(使用- caddy hash-password计算)
- <salt_base64>计算hash密码使用的salt的base64编码后的值
示例
books.alanwei.com {
	reverse_proxy localhost:8088
	basicauth * {
		alan JDJhJDE0JHhpVUtuU09za3dGT1F6RnlMOUlycE9kTXhoYWlOQmVleVl0dzFJczdpdkF2cUdFY3JSN3RL
	}
}
redir
HTTP redirect, 返回3xx跳转响应
语法
redir [<matcher>] <to> [<code>]
- <to>跳转的目标地址, 输出在响应头的- Location中
- <code>HTTP响应状态码, 允许的值如下- 3xx 的整数
- permanent永久跳转(响应状态码 301)
- temporary临时跳转(响应状态码 302, 默认行为)
- html响应一段HTML文档在客户端执行跳转
 
示例
- redir https://example.com所有请求跳转到- https://example.com
- redir https://example.com{uri}效果同上, 但是会保留URI
- redir https://example.com{uri} permanent效果同上, 但是响应状态码是 301
rewrite
在内部进行请求转发, 客户端无感知, 类似代理转发.
语法
rewrite [<matcher>] <to>
- <to>设置请求的URI, 仅仅指定的部分被替换. URI 路径是- ?之前的字符串. 如果- ?是缺省的, 全部部分都是path.
示例
- rewrite * /foo.html转发所有请求到- foo.html,
reverse_proxy
用于设置反向代理
语法
reverse_proxy [<matcher>] [<upstreams...>] {
    # backends
    to      <upstreams...>
    dynamic <module> ...
    # load balancing
    lb_policy       <name> [<options...>]
    lb_try_duration <duration>
    lb_try_interval <interval>
    # header manipulation
	trusted_proxies [private_ranges] <ranges...>
    header_up   [+|-]<field> [<value|regexp> [<replacement>]]
    header_down [+|-]<field> [<value|regexp> [<replacement>]]
    # round trip
    transport <name> {
        ...
    }
	# optionally intercept responses from upstream
	@name {
		status <code...>
		header <field> [<value>]
	}
	replace_status [<matcher>] <status_code>
	handle_response [<matcher>] {
		<directives...>
		# special directives only available in handle_response
		copy_response [<matcher>] [<status>] {
			status <status>
		}
		copy_response_headers [<matcher>] {
			include <fields...>
			exclude <fields...>
		}
	}
}
- header_up对传递给后端的请求头进行设置、添加、删除或者执行替换
- header_down对后端返回的响应头进行设置、添加、删除或者执行替换
默认情况下, Caddy会把所有请求头未经修改地全部传递到后端服务, 包括Host请求头, 下面三种情况不会传递Host请求头:
- 添加或修改了 HTTP header X-Forwarded-For
- 设置了 HTTP header X-Forwarded-Proto
- 设置了 HTTP header X-Forwarded-Host
默认情况下, caddy会忽略请求头中的 X-Forwarded-*, 以防止客户端欺骗. 如果caddy不是连接客户端的第一层(比如在caddy的上层还有CDN), 可以配置 trusted_proxies 信任的IP列表, 为了方便可以设置成trusted_proxies private_ranges信任所有的私有IP段.
简单点说就是caddy为了安全, 会忽略客户端
X-Forwarded-*请求头, 默认不会把这些请求头传递给后端, 但是考虑到caddy有可能不是直接接触到客户端, 在客户端和caddy之间可能有cdn, 所以可用通过配置trusted_proxies解决这种场景.
示例
将 weblog.alanwei.com 的请求转发到 http://localhost:8089:
weblog.alanwei.com {
	reverse_proxy http://localhost:8089
}
将demo.alanwei.com转发到localhost:1337, 并修改后端收到的Host请求头.
demo.alanwei.com {
	reverse_proxy localhost:1337 {
		header_up Host {upstream_hostport}
		header_up X-Forwarded-Host {host}
	}
}
在代理之前对路径进行裁剪:
demo.alanwei.com {
	handle_path /prefix/* {
		reverse_proxy localhost:9000
	}
}
handle
handle和nginx中的location指令很类似, 只有第一个被匹配到的handle block会被执行(多个handle block之间互斥). handle block 可以嵌套(仅限HTTP handle).
语法
handle [<matcher>] {
	<directives...>
}
- <directives...>HTTP处理指令块列表, 一行一个指令, 和handle block外的指令使用方式一样
除了 handle 指令, 还有 handle_path 指令, 不同之处在于, handle_path 在处理之前会截断请求的路径.
route包括其他指令时, 和handle做的事情很像, 但是有两点不同, 1) route block彼此之间不是互斥的, 2) route 内的指令不会被重新排序.
示例
/foo/ 的请求转发到静态文件服务, 其他请求转发到代理服务上:
sample.alanwei.com {
	handle /foo/* {
		file_server
	}
	handle {
		reverse_proxy 127.0.0.1:8080
	}
}
handle 和 handle_path 可以在同一个站点中同时使用, 而且彼此之间依然互斥:
http://location:8090 {
	handle_path /foo/* {
		# The path has the "/foo" prefix stripped
	}
	handle /bar/* {
		# The path still retains "/bar"
	}
}
tls
示例
- 使用自定义证书: tls cert.pem key.pem
- 使用本地自签名证书: tls internal
QA
启动失败
执行caddy命令如果提示以下错误, 需要使用sudo执行:
loading initial config: loading new config: http app module: start: tcp: listening on :443: listen tcp :443: bind: permission denied
如果外部网络无法访问caddy伺服的web, 有可能是系统防火墙拦截了, 如果是linux系统可以尝试使用ufw allow开放端口号.
禁用HTTPS
全局禁用设置全局选项即可:
{
	auto_https off
}
禁用特定域名的https, 只需要在域名地址前指定协议即可:
http://localhost:3009 {
	root * /project_dir
	file_server
}
http://www.alanwei.com {
	reverse_proxy localhost:3009
}
本地HTTPS
有时候本地开发需要用到HTTPS协议的地址,特别是用到了JS的 Security API。 下面介绍如何配置本地的HTTPS请求。
假如想将 https://f0.alanwei.com 代理到本地的 http://localhost:8090 :
- 配置 /etc/hosts(Windows系统是C:\Windows\System32\drivers\etc\hosts)新增一行:127.0.0.1 f0.alanwei.com
- 执行 caddy trust信任证书
- 修改 Caddyfile 文件, Site definition 使用 https://协议,同时增加tls internal配置,示例如下:
{
	http_port 80
	https_port 443
}
https://f0.alanwei.com {
        tls internal
        reverse_proxy http://localhost:8090
}
如果移动端访问会提示证书无效,所以移动端需要信任 caddy 在本机创建的证书,该证书位于 Caddy Data 目录下的 pki/authorities/local 目录,Caddy Data 目录如下:
| OS | Caddy Data Directory |
| Linux, BSD | $HOME/.local/share/caddy |
| Windows | %AppData%\Caddy |
| macOS | $HOME/Library/Application Support/Caddy |
| Android | $HOME/caddy (or /sdcard/caddy) |
示例
{
	http_port 80
	https_port 443
	email me@alanwei.com
	admin :2019
	log {
	        level INFO
	        format json {
	                time_format wall
	        }
	        output file caddy.log {
	                roll_size 10MiB
	                roll_keep 10
	                roll_keep_for 2160h
	        }
	}
}
http://local1.alanwei.com {
	reverse_proxy http://127.0.0.1:5002
	log
}
http://local2.alanwei.com {
	reverse_proxy localhost:5003
	log
}
http://local3.alanwei.com local4.alanwei.com :8090 {
	reverse_proxy localhost:3000
	log
}
files.alanwei.com :8092 {
        root * D:\temporary # 指定目录
        file_server browse {
                index "REAME.md" index.html index.txt # 设置首页文件
                hide "**/node_modules/**" # 隐藏特定文件或目录, 支持占位符和glob patterns
        }
        log
}