
证书过期这件事,从来不是在你盯着的时候发生的。它总是在某个周五晚上,从一个打不开的页面开始。
把三个小站从 Nginx 换到 Caddy,我最初的理由很土:不想再写 certbot 的续期钩子。换完之后省下的不是性能,是每个月那一次「证书还有几天」的心算。
1. 先说清楚它不适合谁
Caddy 2 的核心卖点是自动申请和续期证书,以及一份能读懂的配置。它不是更快的 Nginx,它是更少配置的 Nginx。同样的机器、同样的静态文件,两者的吞吐差距在小站上量不出来。
代价也很实在。Caddy 的资料量远小于 Nginx,遇到奇怪问题时能搜到的答案少;它的模块化编译方式对习惯了发行版包管理的人不太顺手;一些高频调优参数(比如连接复用、缓冲区大小)在 Caddy 里要么没有,要么换了名字。
所以边界是:三到十个站点、单机、不需要精细调优、希望证书自动化,用 Caddy 划算。如果是高并发网关,或者团队里所有人都熟 Nginx,换过来是净亏。
| 要做的事 | Nginx 写法 | Caddy 写法 |
|---|---|---|
| 申请证书 | certbot 插件 + 定时任务 | 写域名,自动完成 |
| 反向代理 | proxy_pass | reverse_proxy |
| 强制跳 HTTPS | 单独一个 80 server 块 | 默认行为 |
| gzip | gzip on 及一串参数 | encode gzip zstd |
| 传真实 IP | proxy_set_header X-Real-IP | 默认带上,需配 trusted_proxies |
2. 三个站的结构
这次要托管的是三类东西:一个纯静态站、一个监听本地端口的应用、一个需要传大文件的后台。三个站共用一台 2 核 4G 的机器,共用一个 Caddy 进程。
/srv/
├── caddy/
│ ├── Caddyfile
│ └── data/ # 证书与 ACME 状态,必须持久化
├── www/
│ ├── static-a/ # 静态站,纯 html
│ └── static-b/
└── apps/
├── api/ # 应用一,监听 127.0.0.1:8080
└── admin/ # 后台,监听 127.0.0.1:9000
端口规划只有一条原则:所有后端只监听 127.0.0.1,对外只开 80 和 443。后端端口一旦对公网开放,反代层的鉴权就形同虚设。
Caddy 的证书和 ACME 账号状态放在 data 目录里。这个目录丢了,第二天续期会重新申请,触发 Let’s Encrypt 的频率限制。备份这个目录,比备份 Caddyfile 更重要。
3. 第一份配置:静态站加反向代理
先装。官方源在各发行版都有包,装完确认版本。本文在 Caddy 2.10 上验证,配置语法在 2.x 内基本稳定,具体小版本可能新增指令。
caddy version
# 预期输出形如 v2.10.x linux/amd64
然后是主配置。三个站点块,共用顶部的全局选项。邮箱一定要写,ACME 到期提醒和账号找回都靠它。
{
email ops@example.com
# 统一日志,便于排查
log {
output file /var/log/caddy/access.log {
roll_size 20MiB
roll_keep 5
}
}
}
static.example.com {
root * /srv/www/static-a
encode gzip zstd
file_server
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
}
}
app.example.com {
encode gzip zstd
reverse_proxy 127.0.0.1:8080
}
注意这里没有一行是关于证书的。Caddy 看到站点块里写了域名,就去申请证书;看到 80 端口的请求,就自动跳 443。这两件事在 Nginx 里各要写一段。
改完先校验语法,再重载。校验命令不会动线上进程,这一步不能省。
caddy validate --config /srv/caddy/Caddyfile
systemctl reload caddy
4. 第二份配置:把真实 IP 传进去
反代一上线,应用日志里的来源 IP 全是 127.0.0.1。这不是 Caddy 的错,是它默认信任了本机。Caddy 会自动带上 X-Forwarded-For,但它默认只信任来自本机的这一跳。
如果前面还有一层 CDN 或者云负载均衡,就必须把那一层的出口网段写进 trusted_proxies。不写,所有访客在应用看来都是同一个 IP,限流会误伤整站。
app.example.com {
reverse_proxy 127.0.0.1:8080 {
# 前面是 Cloudflare 时,信任它的出口网段
trusted_proxies 173.245.48.0/20 103.21.244.0/22 104.16.0.0/13
# 只采信这两个头,其余一律丢弃
header_up X-Real-IP {remote_host}
header_up X-Forwarded-Proto {scheme}
}
}
这里有个容易忽略的细节:{remote_host} 是占位符,由 Caddy 在运行时替换。占位符写错不会报错,只会安静地把字面量发给后端,日志里出现一个叫 {remote_host} 的「IP」。
验收方法很直接:随便访问一次,然后看应用日志里的 IP 是不是你自己的公网出口。如果是 127.0.0.1 或者一串网段,说明信任链没接上。
5. 第三份配置:大文件与超时
后台那个站要传 200MB 左右的视频素材。默认配置下,上传会在中途断掉,返回 413 或者 502。413 是请求体超限,502 是后端等太久已经断了连接,两个错误的处理点不同。
admin.example.com {
encode gzip
reverse_proxy 127.0.0.1:9000 {
transport http {
read_timeout 300s
write_timeout 300s
}
}
request_body {
max_size 512MB
}
}
三个数字要一起调:Caddy 的 max_size、Caddy 到后端的超时、后端自己框架的上传限制。只改前两个,请求会卡在后端的框架层,表现是「上传到 99% 就停住」。
另外,反向代理大文件时会先把请求体落到临时目录。临时目录在小内存机器上常常挂在 tmpfs,512MB 的上限配上 4G 内存,两个人同时传就会把内存打满。要么调小上限,要么把临时目录指到磁盘。
6. 装成服务
发行版的包通常自带 systemd 单元。如果是自己下的二进制,需要手写一份。关键是 -c 指配置、--adapter caddyfile 指格式,两个都不能少。
[Unit]
Description=Caddy
After=network.target network-online.target
Requires=network-online.target
[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/local/bin/caddy run --environ --config /srv/caddy/Caddyfile --adapter caddyfile
ExecReload=/usr/local/bin/caddy reload --config /srv/caddy/Caddyfile --adapter caddyfile
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
CAP_NET_BIND_SERVICE 这一行是为了让非 root 用户能绑 80 和 443。去掉它又用 caddy 用户跑,服务会起不来,报权限不足,而错误日志只会出现在 journal 里,很容易被当成配置错误。
7. 验收:五条命令
systemctl status caddy—— 看 active (running),不是 activating。caddy validate --config /srv/caddy/Caddyfile—— 语法通过。- 浏览器打开三个域名,确认证书颁发者是 Let’s Encrypt,不是「Caddy Local Authority」。
curl -I https://app.example.com—— 看 200 和 server 头。- 翻
/var/log/caddy/access.log,确认三条域名的请求都进来了。
第三条最容易出问题。看到 「Caddy Local Authority」 说明它在用内部 CA 自签,也就是 ACME 没走通。这时候页面能打开、浏览器有警告,很多人会以为是浏览器的问题。
8. 四个必然踩到的坑
坑一:80 端口被占或者被云厂商拦。HTTP-01 验证需要 Let’s Encrypt 从外面访问 80 端口。安全组没开 80、或者 80 上还挂着旧的 Nginx,验证就会失败。
如果 80 确实用不了,可以切到 DNS-01,用 TXT 记录验证。这条路需要装对应 DNS 服务商的插件,官方镜像里不一定带,得自己编译。
坑二:域名解析还没生效就启动。ACME 验证失败后 Caddy 会重试,短时间内反复失败会触发 Let’s Encrypt 的失败次数限制,一周内同一域名只能失败五次左右。等解析生效再启动,或者先关掉自动 HTTPS 调试配置。
坑三:handle 与 route 的顺序。Caddy 的指令按特定顺序求值,不按你写的顺序。写了 handle 又想加全局 header,header 可能先执行,导致它作用在错误的路径上。需要精确控制顺序时用 route 包起来。
坑四:reload 不等于重启。改 Caddyfile 后 systemctl reload 是优雅重载,配置有语法错误时它会保留旧配置继续跑,命令返回成功。所以 validate 必须单独跑,不能靠 reload 的返回值判断。
这四个坑里,前两个是环境问题,后两个是理解问题。环境问题查日志能看到,理解问题不会报错,只会让配置悄悄不生效。
写在最后
把证书这件事交给软件,省下的其实是注意力。以前每到月底,我会下意识点开一次站点看看锁头还在不在;现在这个动作消失了。
但自动化也有它的代价:你不知道它什么时候会不管用,直到它真的不管用。Caddy 把 ACME 藏进了一行域名背后,代价是出问题时你要重新学一遍 ACME。
所以真正的建议不是「都换成 Caddy」,而是:让配置短到能一次读完,让故障点少到能一个一个数出来。工具选哪个,是这之后的事。
你的证书续期现在是自动的,还是每年手动续一次?上一次续期失败,你是怎么发现的?

评论 (0)