本文探讨了使用Caddy 2替代Nginx作为独立项目网关的优势与挑战。文章分享了作者在迁移过程中遇到的证书配额耗尽、日志IP识别错误以及WebSocket配置简化等问题,并提供了相应的解决方案,如容器数据持久化、配置可信代理和WebSocket代理简化等。
过去几年我给自己的独立项目配网关,习惯了在云服务器上装 Nginx。
每次新起一个子域名或者部署个新服务,流程都差不多:先在 Nginx 的 conf.d 下面手写一个反向代理配置文件,加上几行经典的 proxy_set_header,再跑一遍 Certbot 申请证书,然后在 crontab 里配个自动续签任务。
这套方案成熟稳定,但做的小工具越来越多之后,维护证书和这一大堆繁琐配置逐渐成了体力活。偶尔碰上证书到期续签失败、或者哪台测试机的 cron 没跑起来,排查起来相当烦人。
后来我把几个日活不大的独立工具和个人站点换成了 Caddy 2。最直观的体验就是爽快:Caddyfile 只需要短短几行,自动申请证书、自动续期、默认开启 HTTP/2 和 HTTP/3。
但在生产环境跑了半年多,我也结结实实踩了几个暗坑。如果不注意这几个细节,服务上线后轻则拿不到客户端真实 IP,重则证书配额直接被消耗殆尽。
容器部署没持久化 data 目录,把证书申请配额耗尽了
这是我刚用 Caddy 跑 Docker 时栽的最大一个跟头。
Caddy 的一大卖点是全自动管理 TLS 证书,只要你的域名正确解析到了服务器公网 IP,首次访问时它就会自动向 Let's Encrypt 申请免费证书并落盘。
当时我的 docker-compose.yml 只挂载了代码目录和 Caddyfile,完全没管内部的证书存储路径。开发初期因为频繁调试配置和重新部署,一天之内把 Caddy 容器重建了十几次。
到了第二天,网站突然报证书错误打不开了。翻开 Caddy 日志一看,全是红色的 ACME 报错:
HTTP 429 urn:ietf:params:acme:error:rateLimited: too many certificates already issued for exact set of domains
Let's Encrypt 对单一域名的证书颁发有严格的频率限制,同一个子域名每周最多申请 5 次。因为我没给容器做证书目录持久化,每次容器销毁重建,Caddy 就会当成一台全新的服务器重新去申请一张新证书,几个回合下来直接撞上了限流墙。
正确的做法是必须把 Caddy 内部的 /data 目录(存放申请到的私钥和证书)以及 /config 目录挂载到宿主机本地磁盘:
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # 开启 HTTP/3 QUIC
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./caddy_data:/data
- ./caddy_config:/config
只要 /data 目录持久化在本地,容器随便怎么重启,Caddy 都会优先使用本地未过期的证书,再也不会无谓触发限流。
挂了 CDN 之后,后端日志拿到的全是 CDN 节点 IP
不少人为了防攻击或者给国内访问加速,会在域名和服务器之间套一层 Cloudflare 或者阿里云 CDN。
用 Nginx 的时候,我们通常会在配置里加上 set_real_ip_from 来提取真实的客户端 IP。换成 Caddy 之后,它的 reverse_proxy 指令虽然默认会自动传递 X-Forwarded-For 头,但如果前面多了一层代理,Caddy 会默认把直接连接它的上一级节点当成客户端。
结果就是我后台的风控限流日志里,所有用户的来源 IP 全部变成了 Cloudflare 的几个固定节点机房 IP,差点把整批正常用户直接误封。
在 Caddy 2 里解决这个问题,需要在全局或者反向代理指令中显式配置可信代理(trusted proxies):
api.example.com {
reverse_proxy 127.0.0.1:8080 {
trusted_proxies 173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22
}
}
告诉 Caddy 这些 IP 段是受信任的 CDN 代理,这样它在处理请求时才会把真正的客户端原始 IP 提取到最前面,传递给后端业务进程。
WebSocket 代理不需要再手动敲升级头
在 Nginx 里配置 WebSocket,几乎每个人都去搜索引擎复制粘贴过那几行固定代码:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
少写一行连接就会握手失败。
而在 Caddy 2 里,reverse_proxy 原生内置了对 HTTP 协议升级(Connection Upgrade)的处理。只要后端服务支持 WebSocket,Caddyfile 只需要普普通通的一行:
chat.example.com {
reverse_proxy 127.0.0.1:3000
}
客户端发起带 Upgrade: websocket 的握手请求时,Caddy 会自动识别并保持长连接双向透传,完全不需要额外加任何配置。这确实给长连接和推送类服务省掉了不少折腾。
后端服务冷启动,Caddy 默认直接喷 502
我手头有几个用 Go 和 Python 写的后台服务,每次更新镜像重新启动时,容器需要花大约两到三秒加载配置和建立数据库连接。
在这几秒钟里,后端端口处于不可用状态。在 Nginx 体系下,我们可以配置 proxy_next_upstream 或者简单的缓冲重试。而在 Caddy 默认配置下,一旦上游连接拒绝,它会立刻毫不客气地向客户端返回 502 Bad Gateway。
为了避免每次发布版本用户都会看到几秒钟的报错页面,可以在 reverse_proxy 里加上简单的重试策略和健康超时:
app.example.com {
reverse_proxy 127.0.0.1:9000 {
dial_timeout 2s
try_duration 5s
try_interval 250ms
}
}
配置了 try_duration 后,当后端服务正在重启时,Caddy 会在 5 秒内以 250 毫秒为间隔反复尝试重连,只要服务在几秒内拉起,用户端只会感受到请求稍微停顿了一下,随后正常响应,彻底告别了短暂的 502 窗口。
一键开启 zstd 和 gzip 压缩
在 Nginx 里开启现代压缩,往往需要单独装模块或者写上一长串包含各种 MIME 类型的 gzip_types 指令。
Caddy 自带了对新一代压缩算法 zstd 以及经典 gzip 的原生支持。配置非常轻巧,只要在站段内加入一行:
blog.example.com {
encode zstd gzip
file_server {
root /var/www/html
}
}
只要客户端浏览器支持(现代 Chrome、Edge 和 Firefox 都支持),Caddy 就会优先使用压缩率更高、解压速度更快的 zstd,传输体积明显比普通 gzip 更小。
我的生产环境通用 Caddyfile 模板
把上面这些避坑点融合起来,我现在管理多个小工具和 API 服务的通用配置结构基本固定成了这样:
{
# 全局配置,记录格式化 JSON 日志
log {
output file /var/log/caddy/access.log {
roll_size 50mb
roll_keep 5
}
format json
}
}
# 静态资源博客站点
blog.example.com {
encode zstd gzip
root * /var/www/blog
file_server
}
# 动态 API 接口服务
api.example.com {
encode gzip
reverse_proxy 127.0.0.1:8000 {
dial_timeout 3s
try_duration 5s
try_interval 200ms
}
}
修改完配置后,直接执行 caddy reload --config /etc/caddy/Caddyfile,秒级无缝生效,现有连接也不会中断。
对于追求极简和低运维负担的中小型项目,Caddy 2 确实能省掉大量的证书运维时间。只要做好目录持久化、搞清楚反向代理在 CDN 环境下的 IP 透传规则,日常用起来非常称手。
文章标题:不想再为 SSL 证书和反代头疼?我把几个小项目换成 Caddy 2 踩过的坑说清楚
文章链接:https://llbbs.cn/jishujaocheng/195.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?