Caddy 反向代理使用笔记

为什么使用 Caddy

在此之前,我一直使用 Nginx Proxy Manager 管理我的 HTTPS 服务与证书申请,NPM 确实是一个不错的项目,它在我的服务器上稳定运行了两年多。

但即便如此,NPM 也存在一些不可忽视的问题。首先就是开发进度缓慢,v2 版本目前已经没有功能性开发了,承诺的 v3 版本一直难产。我从不对开源项目的 Roadmap 苛求,但是从个人角度出发确实有必要考虑迁移到其他项目之上。Caddy 一直是我的备选,因此在前不久 NPM 迁移过程中又遇到登陆界面提示 Bad Gateway 时,我选择放弃 NPM,尝试 Caddy。

Caddy 与 NPM 其实是两个完全不同的东西,Caddy 更像是 Nginx 本身,没有图形界面,不能在网页上点点鼠标就完成。但是 Caddyfile 的存在又简化了整体的配置流程,对于熟悉终端的人来说甚至配置起来会比 NPM 还要方便。当然对于有些人来说,SSH 到服务端是整件事里最大的障碍,当前版本的 Caddy 恐怕并不适合这些人去使用。

Docker Compose

在整个 Caddy 部署过程中,我遇到的第一个也是唯一一个坑就是 Caddy 的官方 Docker 镜像是最小化构建的,这就导致我所需要的 Cloudflare 模块并不包含在该镜像中。当然解决办法也很简单,就是自己使用官方提供的 builder 构建包含所需模块的镜像,我用 Github Action 每日构建最新镜像,并推送到了 ghcr.io/sixiaolong1117/my-caddy:latest,这样就随时可以拉取到包含 Cloudflare 模块的 Caddy 镜像。

对于任何需要其他模块的人,可以 Fork 我的 Repo 并按照 README 中的要求修改脚本,来构建自己的镜像。

当解决了镜像问题之后,以下是我的 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
services:
caddy:
image: ghcr.io/sixiaolong1117/my-caddy:latest
container_name: caddy
restart: always
cap_add:
- NET_ADMIN
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- ./site:/srv
- caddy_data:/data
- caddy_config:/config
environment:
- TZ=Asia/Shanghai
- DNS=223.5.5.5,8.8.8.8
- CLOUDFLARE_API_TOKEN=${CF_API_TOKEN}
- http_proxy=http://192.168.0.1:8080
- https_proxy=http://192.168.0.1:8080
- all_proxy=socks5://192.168.0.1:8080
- no_proxy=localhost,127.0.0.1,172.22.0.0/16,192.168.0.0/24

volumes:
caddy_data:
caddy_config:

需要注意几点:

  1. ./conf 映射到了容器内的 /etc/caddy,这就是之后的 Caddyfile 所在位置。
  2. /data/config 被我映射到了由 Docker 管理的卷中,这些目录的内容主要是证书和其他数据,因此需要持久化存储,但又不是特别重要(Caddyfile 保存了所有重要的配置信息,证书本身由 Caddy 负责管理即可)。
  3. 环境变量中的 CLOUDFLARE_API_TOKEN=${CF_API_TOKEN} 要求在 docker-compose.yml 同目录创建 .env 文件,内容写 CF_API_TOKEN=<你的 CF TOKEN>,该 TOKEN 应有 DNS 编辑权限。
  4. http_proxy=https_proxy=all_proxy=no_proxy= 所设置的代理是为了加速访问,否则 Caddy 可能会卡在 DNS 验证或证书申请。

Caddyfile

./conf 创建文件 Caddyfile,我们所有的反代配置都会写到这一个文件中。

联系邮箱(可选)

1
2
3
{
email [email protected]
}

这部分是因为 Let’s Encrypt 需要提供一个邮箱用于接收证书到期提醒、紧急安全通知,同时作为证书注册的联系人信息。如果不设置,则 Caddy 默认会用一个匿名邮箱(如空邮箱),不过不推荐那样做。

Caddyfile 语法

Caddyfile实际上是很简单的,其语法如下:

1
2
3
example1.com example2.com {
// What you want to do.
}

简单来说就是给定域名,然后说需要 Caddy 负责处理什么。比如最简单的,我们需要 Caddy 为域名 example1.com 申请证书(通过 Cloudflare DNS-01 验证),并返回 “Hello, world!”,那我们可以这么写:

1
2
3
4
5
6
7
example1.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}

respond "Hello, world!"
}

此处通过 {env.CLOUDFLARE_API_TOKEN} 调用前文配置要求填写的 TOKEN。

而对于反代的配置,也就是修改 respond 部分,比如反代本地端口 8080 的服务:

1
2
3
4
5
6
7
example1.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}

respond 127.0.0.1:8080
}

其他返回值同理。

通常情况下我们不会单独为每个域名申请证书,尤其是在只有一个域名的情况下使用子域名,因为证书信息是公开可查询的,一个域名下所有子域名证书都会被查询到。个人使用的情况下这相当于暴露了所有反代的服务,因此通常会使用泛域名来解决这个问题。Caddy 也支持泛域名证书的申请,不过如果是用作反代的话就需要单独写 respond 部分:

1
2
3
4
5
6
7
8
9
*.example1.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}

service1.example1.com {
respond 127.0.0.1:8080
}

如此,我们就完成了对 *.example1.com 的泛域名证书申请,同时让 8080 端口的服务可以通过 service1.example1.com 访问。

一个相对完整的 Caddyfile 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
email [email protected]
}

*.example1.com {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}

service1.example1.com {
respond 127.0.0.1:8080
}

service2.example1.com {
respond 127.0.0.1:9090
}

启动服务

在写完 Caddyfile 并保存之后,在 docker-compose.yml 同目录运行:

1
docker compose up -d

即可启动服务,日后修改 Caddyfile 之后也需要运行:

1
docker compose restart

来重启服务。

对于不方便 SSH 访问服务端的人来说,可以将 Caddyfile 远程挂载到其他设备(如通过 WebDAV 挂载到手机文件管理器上),然后通过 SSH 脚本(如苹果快捷指令)来运行 Docker 命令。需要注意远程挂载文件的安全性问题,这并非本笔记所涉及范围,不做赘述。

其他

很多服务在反代时都要求传递 header,一些网盘服务需要修改上传文件大小,在 Caddy 下可以这么配置:

1
2
3
4
5
6
7
8
9
10
11
openlist.example1.com {
reverse_proxy 127.0.0.1:18188 {
header_up Host {host}
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}
request_body {
max_size 10GB
}
}