家里那台小主机上现在挂着四五个服务:影视库、下载器、网盘同步、路由器的管理页。手机上一个一个存 192.168.1.23:8096 这种地址,存到第四个就记混了;浏览器打开还一律标「不安全」,登录框旁边挂个红三角。
想把这些东西收到一个域名下面、配一张能用的证书,传统做法是 nginx 加 certbot。我用了两年,最大的问题不是难写,是脆——续期任务哪天悄悄失败一次,第二天全套服务集体打不开。
这次换成 Caddy 2.11.7 从头做了一遍,顺手把手上的数据都量了一下。
一个 exe,没有依赖
Windows 包下载下来 17.6MB,解压出来只有一个 caddy.exe,53.2MB。没有安装程序,没有运行时依赖,扔进任意目录就能跑。caddy version 报 v2.11.7,caddy list-modules 数下来 137 个模块,编译它的 Go 是 1.26.8。
模块数量这件事值得说一句:Caddy 的功能全是模块拼起来的,137 个意味着标准版自带 file_server、reverse_proxy、encode,以及三套证书签发器(ACME、ZeroSSL、本地 CA),不用另外装插件。
Caddyfile 就这几行
我的配置最后长这样,两个服务、一个域名后缀:
{
admin off
}
media.home:8443 {
tls internal
encode zstd gzip
reverse_proxy 127.0.0.1:8096
}
pan.home:8443 {
tls internal
reverse_proxy 127.0.0.1:8384
}
media.home 和 pan.home 要在路由器 DNS 或者系统 hosts 里指向小主机的内网地址,否则名字解析不到。tls internal 的意思是用 Caddy 自建的本地 CA 签证书,不联网、不等审核、不需要 80 端口。
同样的事用 nginx 写,是 server 块、listen 443 ssl、ssl_certificate 和 ssl_certificate_key 两行、proxy_pass 再加一堆 header,外面还挂一个 certbot 的续期定时任务和 reload 钩子。Caddy 把这几件事收进了一个 tls 指令。
改完配置不用重启,caddy reload 一下就行。
实测:启动、吞吐、内存、压缩
机器是 Ryzen 5 3550H、16GB 内存、SATA SSD,全程本机回环,不经过网线,所以看的是软件本身的开销,不是你家的实际带宽。
| 项目 | 结果 |
| --- | --- |
| 解压后主程序 | 53.2MB |
| 冷启动到第一个 200 响应 | 371ms |
| 首次 HTTPS 就绪(含生成本地 CA 并签发证书) | 412ms |
| TLS 握手(time_appconnect) | 18~25ms |
| 内存:空载 / 传完 200MB 后 | 41.7MB / 49.2MB |
吞吐量的是 200MB 单个文件、单连接、各跑六轮取中位数:
| 路径 | 吞吐 |
| --- | --- |
| node 直接发文件 | 168.4 MB/s |
| 经 Caddy 反向代理 | 117.5 MB/s |
| Caddy 自己发文件(file_server) | 275.0 MB/s |
两个结论和直觉不一样。
第一,Caddy 自己发文件比 node 快 63%。 原因不神秘:node 那边是逐块读流再写 socket,中间要过一层 JS;Caddy 是 Go 的零拷贝发送,文件又在系统缓存里,275MB/s 差不多就是单连接被内存带宽限制后的样子。
第二,反代比直连慢 30%。 多了一跳,数据要从上游读进来再写出去,多一次用户态拷贝。这是反代的固有成本,不是配置问题,nginx 做同样的事也躲不开。
还有人关心内存会不会跟着文件涨。我连续传了 18 次 200MB(合计 3.6GB 流量),RSS 从 41.7MB 涨到 88.8MB 就不再动了,没有出现把整个文件读进内存的情况。Go 的 GC 不急着把内存还给系统,数字偏高是正常的,不是泄漏。
压缩这块也顺手量了。encode zstd gzip 会按客户端支持的编码挑,我拿一个 6.44MB 的中文 HTML,直接数服务器返回的原始字节:
| 请求头 Accept-Encoding | 返回的编码 | 体积 |
| --- | --- | --- |
| identity | 不压 | 6,440,000 |
| gzip | gzip | 35,916 |
| zstd | zstd | 3,811 |
| gzip, zstd | zstd | 3,811 |
得说明这段文本是同一行重复了四万次,属于病态样本,9 倍这个差距不能套到普通网页上——真实页面里 zstd 通常只比 gzip 小一成多。但它说明了一件事:客户端两种都支持时,Caddy 会选 zstd,不用你手动排顺序。
坑在别处:我一开始写的是 encode zstd gzip br,因为 brotli 压静态资源确实更狠,结果配置连解析都过不去,报 module not registered: http.encoders.br。标准版没编 brotli,要么换带插件的构建,要么老实写两种。
证书:本地 CA、真域名、自签上游
tls internal 走本地 CA。第一次跑的时候 Caddy 生成一对根证书和中间证书,放在 %APPDATA%\Caddy\pki\authorities\local\ 下面,root.crt 只有 627 字节。从进程启动到 HTTPS 能访问,实测 412ms。
代价是你得把这个 root.crt 装进每台要访问它的设备的信任列表里——Windows 是「受信任的根证书颁发机构」,iPhone 要装描述文件,再去「关于本机 → 证书信任设置」手动打开。少装一台,那台设备照旧报警。局域网里设备一多,这一步比写配置本身费时间。
有公网域名和 80/443 的话就简单了:把 tls internal 删掉、地址写成真域名,Caddy 自己去申请、自己去续,你什么都不用管。这也是它最值钱的地方——证书的生命周期不归你管了。
再往下就是自签设备。我家路由器后台和几台老设备是自签证书,第一次反代过去直接 502,日志里写着:
tls: failed to verify certificate: x509: certificate signed by unknown authority
Caddy 默认校验上游证书,这一条把所有自签设备都拦在门外。加一段 transport 就好:
reverse_proxy https://192.168.1.1:443 {
transport http {
tls_insecure_skip_verify
}
}
tls_insecure_skip_verify 只影响 Caddy 到上游这一段,浏览器到你这一段还是正常校验,别被名字吓到。当然它确实关掉了对上游的身份验证,只应该在纯内网的设备上用。
五个坑,都踩过
点文件默认能访问。 我在站点根目录放了个 .hidden.txt,直接请求返回 200 和全文。file_server 不自动拦点号开头的文件,得自己加规则挡掉,不然 .git、.env 这类东西谁都能下。
管理接口默认开着。 不写 admin off,启动后 127.0.0.1:2019 会监听,任何本机程序都能通过它改配置。本机可信就无所谓,但小主机上还跑着别的东西时,关掉更省心。
HTTP/3 是默认开的。 响应头里带 Alt-Svc: h3=":8443",说明 QUIC 已经在 UDP 上等着了。防火墙或者路由器只放了 TCP 的话,浏览器会先试 HTTP/3 失败再退回 HTTP/2,首屏白等几百毫秒。
全局 auto_https off 和 tls internal 不能一起用。 我第一版在全局块里写了 auto_https off,结果带 tls internal 的站点直接 adapt 失败,报证书策略冲突。要么去掉全局那行,要么给 HTTPS 站点写明确的主机名。
caddy start 后台跑要重定向输出。 直接接管道调用会挂住不返回,重定向到文件就是 559ms 返回并打印 pid。另外它的数据目录跟着 Windows 用户走,换个账号登录就是一套空配置。
什么情况下别用它
如果你只有一个服务、只想从外网访问,Caddy 是绕远路。那种场景用 frp 或者路由器端口转发,五分钟搞定,还不用管证书。
Caddy 的价值在服务变多之后:统一域名、统一入口、一张证书、一处改配置。等到你手机书签里躺着七八个 IP 和端口的时候,这几行配置的收益才显出来。
你们家里那堆服务是怎么统一入口的,还是继续记端口?