Skip to main content

Caddy 简介

· 21 min read
Alan

官方给的简介是: Caddy是一个强大的、可扩展的平台, 用于伺服你的站点、服务以及应用. Caddy使用Go语言编写.

简单来说, caddy 和 nginx 很像, 我觉得相比较nginx有以下优点:

  1. 安装简单 caddy的是个单文件可执行程序, 没有任何依赖, 下载下来就可以使用. 对于简单的web服务, 使用命令行即可运行, 不需要任何配置文件.
  2. 自动签发HTTPS证书 caddy默认对所有站点开启HTTPS(支持Let's encrypt 和 ZeroSSL证书自动申请).
  3. 原生支持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):

Caddyfile
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 基本配置如下:

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的文件结构如下:

caddyfile

note
  • 文件开始的全局配置块(global options block)是可选的
  • 如果没有全局配置块, Caddyfile 文件的第一行一定是需要伺服的站点的地址(address)
  • 如果文件仅有一个站点配置块(site block), 那配置块的花括号({ })是可以省略的

Caddyfile 至少有一个站点配置块(site block), 如果指令(directive)出现在站点配置块的地址(address)前, 那配置文件解析将会失败.

blocks

使用花括号开启和关闭一个block:

... {
...
}
  • 左花括号({)必须在行尾
  • 右花括号(})必须独占一行

当配置文件仅有一个站点配置块(site block), 为了方便快速定义单个站点, 花括号(和block的内容缩进)是可选的, 比如:

Caddyfile
localhost

reverse_proxy /api/* localhost:9001
file_server

等价于

Caddyfile
localhost {
reverse_proxy /api/* localhost:9001
file_server
}

如果有多个站点, 必须使用花括号:

Caddyfile
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:

下面是禁用Caddy默认HTTPS伺服行为的方法:

  • 显式禁用
  • 在配置文件中不提供任何主机名或IP
  • 地址使用非HTTP端口号(80, 443)
  • 地址协议使用 http
  • 手动加载证书

在Caddyfile配置文件的全局配置里增加auto_https off即可显式禁用caddy所有HTTPS伺服行为.

Local HTTPS

为了通过HTTPS伺服非公开站点(即本地站点), Caddy使用自签名证书, 签名证书存储在 Caddy's data directorypki/authorities/local 目录下.

Caddy的数据目录(data directory)默认路径为:

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

示例

Caddyfile
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请求头:

  1. 添加或修改了 HTTP header X-Forwarded-For
  2. 设置了 HTTP header X-Forwarded-Proto
  3. 设置了 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
}

示例

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