请点击上方蓝字TonyBai订阅公众号!

Go语言诞生十多年来,社区涌现出众多优秀的Web服务器和反向代理解决方案。其中,最引人注目的无疑是Caddy[2]和Traefik[3]。这两者都为开发者和系统管理员提供了更简单、更安全的现代化Web服务器和反向代理部署选项。尽管它们的目标略有不同,Caddy最初旨在满足开发者快速搭建反向代理的需求,特别关注配置的简易性,并在后期增加了自动HTTPS和全面的API支持;而Traefik则更强调云原生架构,适合基于微服务的应用,尤其是使用Docker或Kubernetes部署的场景,提供动态服务发现和灵活的路由能力。
我于2015年首次体验了开源发布的Caddy[4],其超简单的配置确实给我留下了深刻的印象。之后也一直关注着Caddy的发展,Caddy在支持通过ACME协议[5]自动为服务的域名获取免费HTTPS证书的功能后,Caddy就被我部署在自己的VPS上[6],为Gopher Daily[7]等站点提供反向代理服务,运行十分稳定。Caddy这一为域名自动获取免费HTTPS证书的功能是其简化站点部署初衷的延续,也为Caddy赢得的广泛的用户和赞誉,并且这一特性不仅使得Caddy在个人项目和小型部署中大受欢迎,也让它在企业级应用中占有一席之地。
近10年后,我打算在这篇文章中再次探索一下Caddy,了解一下如今的Caddy都提供哪些强大的功能特性,为后续更好地使用Caddy做铺垫。
注:Caddy发展了近10年,支持了很多标准特性以及非标准特性(由社区提供,caddy官方不提供保证和support),这里仅就笔者感兴趣的特性做探索。目前Caddy依靠sponsor的赞助[8]进行着可持续演进,其所有标准功能都是免费的,但其作者Matt Holt[9]也会为企业级赞助商进行定制功能开发。
1. Caddy的运行方法与基本配置
1.1 Caddy的启停
Caddy使用Go开发,因此继承了Go应用部署的一贯特点:只有一个可执行文件。将下载的Caddy放到$PATH路径下,我们就可以在任意目录下执行它了:
$caddy version
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=
$caddy run
2024/10/11 07:56:24.664 INFO admin admin endpoint started {"address": "localhost:2019", "enforce_origin": false, "origins": ["//127.0.0.1:2019", "//localhost:2019", "//[::1]:2019"]}
这么启动后,caddy就会作为一个前台进程一直运行着,直到你停掉它。当然,我们也可以使用start命令将caddy作为后台进程启动:
$caddy start
2024/10/11 08:32:07.557 INFO admin admin endpoint started {"address": "localhost:2019", "enforce_origin": false, "origins": ["//127.0.0.1:2019", "//localhost:2019", "//[::1]:2019"]}
2024/10/11 08:32:07.557 INFO serving initial configuration
Successfully started Caddy (pid=31215) - Caddy is running in the background
使用stop命令可以停到该后台进程:
$caddy stop
2024/10/11 08:32:37.043 INFO admin.api received request {"method": "POST", "host": "localhost:2019", "uri": "/stop", "remote_ip": "127.0.0.1", "remote_port": "65178", "headers": {"Accept-Encoding":["gzip"],"Content-Length":["0"],"Origin":["http://localhost:2019"],"User-Agent":["Go-http-client/1.1"]}}
2024/10/11 08:32:37.043 WARN admin.api exiting; byeee!!
2024/10/11 08:32:37.043 INFO admin stopped previous server {"address": "localhost:2019"}
2024/10/11 08:32:37.043 INFO admin.api shutdown complete {"exit_code": 0}
1.2 使用Caddyfile配置站点信息
不过如此启动后的caddy并没有什么卵用,因为没有任何关于站点的配置信息。但caddy提供了config API(默认使用2019端口),我们可以使用下面方式访问该API:
$curl localhost:2019/config/
null
由于没有任何配置数据,该接口返回null。Caddy提供了强大的API可以在Caddy运行是动态设置站点配置信息,这个我们后续再说,因为首次使用Caddy时,开发者通常更愿意使用Caddyfile来提供初始配置信息,Caddyfile也是最初caddy开源时唯一支持的配置方式。我们以server1.com为例来看看在本地使用caddy为其建立反向代理有多简单。下面是Caddyfile的内容:
server1.com {
tls internal
reverse_proxy localhost:9001
}
然后我们基于该Caddyfile启动caddy,如果不显式传入配置文件,caddy默认使用当前目录(cwd)下的Caddyfile作为配置文件:
$caddy run
2024/10/11 08:49:36.916 INFO using adjacent Caddyfile
2024/10/11 08:49:36.920 INFO adapted config to JSON {"adapter": "caddyfile"}
2024/10/11 08:49:36.926 INFO admin admin endpoint started {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2024/10/11 08:49:36.928 INFO tls.cache.maintenance started background certificate maintenance {"cache": "0xc0005add80"}
2024/10/11 08:49:36.936 INFO http.auto_https server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS {"server_name": "srv0", "https_port": 443}
2024/10/11 08:49:36.936 INFO http.auto_https enabling automatic HTTP->HTTPS redirects {"server_name": "srv0"}
2024/10/11 08:49:36.964 WARN pki.ca.local installing root certificate (you might be prompted for password) {"path": "storage:pki/authorities/local/root.crt"}
2024/10/11 08:49:37.024 INFO warning: "certutil" is not available, install "certutil" with "brew install nss" and try again
2024/10/11 08:49:37.024 INFO define JAVA_HOME environment variable to use the Java trust
Password:
2024/10/11 08:49:41.629 INFO certificate installed properly in macOS keychain
2024/10/11 08:49:41.629 INFO http enabling HTTP/3 listener {"addr": ":443"}
2024/10/11 08:49:41.632 INFO http.log server running {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2024/10/11 08:49:41.632 INFO http.log server running {"name": "remaining_auto_https_redirects", "protocols": ["h1", "h2", "h3"]}
2024/10/11 08:49:41.632 INFO http enabling automatic TLS certificate management {"domains": ["server1.com"]}
2024/10/11 08:49:41.656 INFO tls cleaning storage unit {"storage": "FileStorage:/Users/tonybai/Library/Application Support/Caddy"}
2024/10/11 08:49:41.656 INFO autosaved config (load with --resume flag) {"file": "/Users/tonybai/Library/Application Support/Caddy/autosave.json"}
2024/10/11 08:49:41.656 INFO serving initial configuration
2024/10/11 08:49:41.657 INFO tls finished cleaning storage units
2024/10/11 08:49:41.657 INFO tls.obtain acquiring lock {"identifier": "server1.com"}
2024/10/11 08:49:41.676 INFO tls.obtain lock acquired {"identifier": "server1.com"}
2024/10/11 08:49:41.676 INFO tls.obtain obtaining certificate {"identifier": "server1.com"}
2024/10/11 08:49:41.684 INFO tls.obtain certificate obtained successfully {"identifier": "server1.com", "issuer": "local"}
2024/10/11 08:49:41.685 INFO tls.obtain releasing lock {"identifier": "server1.com"}
2024/10/11 08:49:41.686 WARN tls stapling OCSP {"error": "no OCSP stapling for [server1.com]: no OCSP server specified in certificate", "identifiers": ["server1.com"]}
这段日志“信息量”很大,我们后面一点点来看。现在我们先验证一下caddy启动后是否能成功访问到server1.com这个“站点”,拓扑图如下:

server1.com的程序如下:
// server1.go
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello, server1.com")
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Server is listening on port 9001...")
if err := http.ListenAndServe("localhost:9001", nil); err != nil {
fmt.Println("Error starting server:", err)
}
}
启动server1后,我们使用curl访问server1.com(注:请先将server1.com放入/etc/hosts中,映射到本地127.0.0.1):
$go run server1.go
$curl https://server1.com
hello, server1.com
是不是非常简单 - 短短几行配置就能在本地搭建出一个可以测试https站点的环境!
1.3 Caddyfile背后的那些事儿
现在是时候基于上面caddy run之后输出的日志以及Caddyfile的内容来说说caddy的一些运行机制了。
首先,当前版本的Caddy的默认配置信息格式已经不再是我们在Caddyfile中看到的那样了,而是改为了json格式。虽然上面我们是基于Caddyfile启动的caddy,但实际上caddy程序会在内部启用caddyfile adapt,将Caddyfile的格式转换为json格式后,再作为配置信息提供给caddy的后续逻辑:

比如上面的Caddyfile被转换为json后的配置如下:
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:9001"
}
]
}

最低0.47元/天 解锁文章
340

被折叠的 条评论
为什么被折叠?



