官方给的简介是: 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}
效果同上, 但是会保留URIredir 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
}
示例
{
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
}