侧边栏壁纸
  • 累计撰写 135 篇文章
  • 累计收到 2 条评论

不想再为 SSL 证书和反代头疼?我把几个小项目换成 Caddy 2 踩过的坑说清楚

2026-9-30 / 0 评论 / 10 阅读
AI 摘要由 AI 生成

本文探讨了使用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 透传规则,日常用起来非常称手。

评论一下?

OωO
取消