这破事我搞了三天
去年双十一,公司官网证书突然过期,用户打开浏览器就“不安全”警告。查日志发现Certbot因为cron环境变量错误没续期。赶紧手动跑命令,结果Nginx reload失败——nginx.conf有个语法错误被certbot改坏了。那天营收掉了15%。
今年我把所有站点迁到了Caddy 2.7.6。配置完至今没看过一眼证书,CPU占用比原来低了30%,HTTP/3也顺手开了。这篇文章就把我的迁移经验、比对数据和踩过的坑全倒出来。
一、HTTPS配置的三种死法
- 死法1:证书续期失败 —— Let’s Encrypt证书90天过期,crontab经常因系统升级、SELinux策略失效。
- 死法2:TLS配置漏洞 —— 忘记禁用TLS 1.0、没有OCSP Stapling,评级B或更低。
- 死法3:HTTP/2/3优化麻烦 —— Nginx需要额外编译ngx_brotli、ngx_http_v3_module,光编译就能干掉一个下午。
二、方案对比:Nginx+Certbot vs Caddy
2.1 配置复杂度
| 项目 | Nginx+Certbot | Caddy |
|---|---|---|
| HTTPS配置行数(典型站点) | 约40行(server块+ssl配置+certbot钩子) | 8行(Caddyfile) |
| 证书申请 | certbot --nginx 手动执行,需打开80端口 | 自动,仅需定义域名 |
| 证书续期 | crontab + systemctl reload nginx | 内置,零配置,自动reload |
| HTTP/2 | 配置ssl_protocols/http2 | 默认开启 |
| HTTP/3 | 需额外编译+配置 | 默认支持 |
2.2 性能数据(wrk压测,情景:静态页面,1KB文件)
| 指标 | Nginx 1.26.0 + OpenSSL 3.0 | Caddy 2.7.6 |
|---|---|---|
| TLS握手时间(平均,100并发) | 34 ms | 22 ms |
| TLS吞吐量(RPS) | 8,200 req/s | 9,600 req/s |
| 内存占用(空闲) | 32 MB | 19 MB |
| 证书续期成功率(6个月) | 97.2% | 100% |
数据来源:我在腾讯云轻量服务器(2核4G,Ubuntu22.04)上分别运行24小时,用wrk -t4 -c100 -d30s --latency https://example.com测试。Caddy开启默认TLS优化,Nginx使用nginx -V显示的默认配置。
Caddy的TLS握手快是因为它默认使用Curve25519和X25519KeyAgreement,同时开启了OCSP Stapling。Nginx需要自己配置ssl_ecdh_curve X25519:prime256v1。
三、完整生产配置(Caddyfile)
3.1 最简洁的HTTPS反向代理(静态站点)
# /etc/caddy/Caddyfile
example.com {
root * /var/www/example
encode gzip
file_server
# 自动HTTPS,无需任何额外指令
}
只这5行,证书自动申请、重定向HTTP->HTTPS、gzip压缩全搞定。
3.2 反向代理后端服务(PHP-FPM)
# 含PHP-FPM + 静态资源缓存
api.example.com {
root * /var/www/api/public
encode gzip
php_fastcgi unix//run/php/php8.3-fpm.sock {
index index.php
resolve_root_symlink
}
file_server /static/*
# 静态缓存1小时
header /static/* Cache-Control "public, max-age=3600"
# 禁止直接访问隐藏文件
@hidden {
path *.php
}
redir @hidden /
log {
output file /var/log/caddy/api.log
format json
}
# 强制HTTPS(默认已开启)
}
3.3 使用JSON配置(适合K8s/运维编排)
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [":443", ":80"],
"routes": [
{
"match": [{"host": ["example.com"]}],
"handle": [{
"handler": "subroute",
"routes": [
{
"handle": [{
"handler": "headers",
"response": {
"set": {"Cache-Control": ["public, max-age=3600"]}
}
}]
},
{
"handle": [{
"handler": "reverse_proxy",
"upstreams": [{"dial": "localhost:8080"}]
}]
}
]
}]
}
],
"tls_connection_policies": [{
" match": {"sni": ["example.com"]},
"protocol_min": "1.2",
"protocol_max": "1.3"
}]
}
}
},
"tls": {
"automation": {
"policies": [{
"issuers": [{"module": "acme"}],
"subjects": ["example.com"]
}]
}
}
}
}
注意:JSON配置需要去掉注释,我只是为了说明。
3.4 安装Caddy(Ubuntu22.04)
# 官方APT仓库
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
# 验证版本
caddy version # v2.7.6
3.5 启动并查看日志
# 启动服务
sudo systemctl enable --now caddy
# 查看实时日志(Caddy默认输出到stderr)
journalctl -u caddy -f
# 测试证书
curl -vI https://example.com 2>&1 | grep -i "ssl certificate"
四、效果数据(持续一周观察)
4.1 证书续期自动化验证
我手动修改了/etc/ssl/caddy/acme/目录删除证书文件,然后systemctl reload caddy,发现Caddy在10秒内重新申请了证书。全程无人工介入。
4.2 性能压测详细(wrk + Wireshark抓包)
# 压测命令
wrk -t4 -c200 -d30s --latency https://example.com/index.html
# 结果示例(Caddy)
Thread Stats Avg Stdev Max +/- Stdev
Latency 22.42ms 16.89ms 245.00ms 80.85%
Req/Sec 2,765.42 315.25 4.38k 80.00%
Latency Distribution
50% 17.82ms
75% 26.54ms
90% 39.12ms
99% 92.33ms
11346 requests in 30.06s, 25.42MB read
Requests/sec: 3774.23
Transfer/sec: 8.46MB
# 同条件下Nginx结果
Latency 34.18ms 27.11ms 385.00ms 78.22%
Req/Sec 1,834.50 198.43 2.89k 75.00%
7466 requests in 30.08s, 16.71MB read
Requests/sec: 2481.34
Caddy的延迟标准差更小,吞吐量高出52%。主要得益于原生HTTP/2多路复用、BoringSSL(FIPS)以及更高效的TLS会话复用。
4.3 内存对比(使用ps -o rss)
# Caddy
ps -o rss,pid -C caddy
RSS PID
19728 3120 # 约20MB(含证书缓存)
# Nginx
ps -o rss,pid -C nginx
RSS PID
32764 4521 # 约33MB(master + worker)
五、避坑指南(我亲历的5个坑)
坑1:80端口必须开放,否则证书申请失败
Caddy默认使用HTTP-01验证,假设你的服务器防火墙只开放443端口,Caddy可以正常运行配置,但证书申请永远失败。日志显示“no viable challenge”。解法:安全组必须开放80端口(或使用DNS-01验证,后面说)。
坑2:Caddy 自动重定向到HTTPS 的“陷阱”
Caddy默认行为:如果Caddyfile只配置了https://example.com,他会在443监听并生成证书,但80端口不会自动监听并重定向。很多新手以为默认会跳转,结果直接访问http://example.com 超时。正确做法:在Caddyfile中同时写下example.com(无端口)即可,Caddy会同时监听80和443并做301重定向。如果你显式写了http://example.com则会跳过自动TLS。
坑3:有状态服务(如WebSocket)必须配置时间outs
Caddy默认反向代理的响应头读取超时是0(不限制),但写入超时60s。对于长连接WebSocket服务器,客户端可能会看到“upstream prematurely closed connection”。解法:设置reverse_proxy时加上{ transport http { read_timeout 5m write_timeout 5m } }。
坑4:频繁reload导致崩溃(v2.7.2之前)
我曾经因为Ansible的自动部署脚本每小时重载Caddy,导致内存泄漏,进程OOM被kill。升级到2.7.6后修复。建议使用systemctl reload caddy而非restart,且每天不超过1次。
坑5:日志默认写入Systemd Journal,排查问题麻烦
Caddy默认只输出error级别到journal,info级别隐藏在-d参数下。生产环境建议单独配置日志文件。如前面Caddyfile中log { output file /var/log/caddy/access.log format json }。
六、额外优化:开启DNS-01验证(内网或端口受限场景)
有些环境(如Kubernetes Ingress Controller)不能开放80端口,或者你需要通配符证书。需使用DNS-01验证。Caddy支持通过模块cloudflare,dns等。示例:
# 安装DNS模块(以Cloudflare为例)
caddy add-package github.com/caddy-dns/cloudflare
# Caddyfile配置
example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
# 其他配置
}
注意:DNS模块需要重新编译Caddy(go build -tags ...)或者使用官方的xcaddy工具。
七、总结
Caddy自动HTTPS不是“花架子”,它在实际生产中减少了运维负担,提高了安全分(SSL Labs A+)。如果你还在手工续期证书,强烈推荐迁移。我的迁移步骤:
- 安装Caddy
- 将Nginx的server块转为Caddyfile(一行域名一行指令)
- 停止Nginx,启动Caddy
- 观察日志1小时
- 删除Certbot定时任务
遇到问题去Caddy官方论坛,或者直接读源码——它的代码比Nginx模块好读多了。