工具与库 · 2026 年 11 月 11 日

Caddy:自动 HTTPS 的极简 Web 服务器

证书申请、续期、重载,本来是三件需要人盯的事。Caddy 把它们做进默认行为,一条 reverse_proxy 就能上线。这篇讲清它省了什么,以及省不掉的前提条件。

给一个小服务配 HTTPS,传统流程是:装 certbot,写 nginx 配置,跑一次签发,配 cron 续期,加一条到期告警,再写一页文档说明证书在哪个目录。任何一步漏了,三个月后就会在半夜收到「证书已过期」的消息。

Caddy 把这些步骤合并成默认行为:启动时自动申请证书,到期前自动续,改了配置热加载。代价是你要接受它的默认值,并保证自动签发的前提成立。

一条配置能做完的事#

caddy
{
    email ops@example.com       # 到期通知和 ACME 账户用
}

example.com {
    encode zstd gzip
    reverse_proxy 127.0.0.1:8080

    log {
        output file /var/log/caddy/access.log
        format json
    }
}

存成 Caddyfile,caddy run 起来,证书就开始了。域名写对、80/443 通,剩下的它自己处理——包括把 HTTP 自动重定向到 HTTPS。

日常操作三条命令:

bash
caddy validate --config Caddyfile    # 只校验语法,不启动服务
caddy fmt --overwrite Caddyfile      # 格式化,统一缩进
caddy reload --config Caddyfile      # 热加载,失败时旧配置继续运行

reload 走的是本地管理接口,不重启进程,连接不会断。配置写错时它报错退出,正在跑的版本不受影响——这一点比手工 nginx -s reload 后对着 502 排查要友好。

Caddyfile 的组织方式#

结构只有三层:全局选项块(放在最前,花括号包住)、站点块(地址加花括号)、站点块里的指令。地址可以写域名,也可以写 localhost 或 :8080。

需要重复的片段用 snippet:括号包围的命名块,用 import 引用。多个站点共享反代、压缩和安全头时,这比复制粘贴可靠得多。

caddy
(common) {
    encode zstd gzip
    header {
        X-Content-Type-Options nosniff
        Strict-Transport-Security max-age=31536000
    }
}

a.example.com {
    import common
    reverse_proxy 127.0.0.1:8080
}

同样的写法可以覆盖十来个站点而不失控。真到这一步,说明该考虑配置生成或换更结构化的方案了。

自动 HTTPS 的前提,一条都不能少#

前提 不满足时的表现
域名 A/AAAA 记录指向本机 挑战无法完成,签发失败
80 或 443 端口可从公网访问 同上,日志里能看到 ACME 错误
前面没有抢占 80 端口的 CDN HTTP 挑战被转发走,验证失败
/data 目录持久化 每次重启重新签发,容易触发频率限制
邮箱配置正确 证书快过期时没人收到通知

第三条最常被忽略。套了 CDN 之后,example.com 的 80 端口到了 CDN 而不是你的机器。解决办法要么关掉 CDN 的代理(仅 DNS),要么用 DNS-01 挑战——后者需要带对应 provider 插件的 Caddy 构建。

第四条在容器里尤其致命。Caddy 把证书和 ACME 账户存在数据目录里,容器不挂卷,每次重建都是一张新证书。Let's Encrypt 对同一域名的签发频率有限制,具体额度以官方文档为准,但反复重建足以把额度耗光,然后你会有几个小时什么都签不下来。

yaml
services:
  caddy:
    image: caddy:2          # 固定大版本,具体版本以官方文档为准
    ports: ["80:80", "443:443"]
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data       # 证书,必须持久化
      - caddy_config:/config
volumes:
  caddy_data:
  caddy_config:

和 Nginx 怎么选#

维度 Nginx Caddy
配置量 反代要写 server/location 若干块 两三行
证书 需要 certbot 或 acme.sh 配合 内置,自动续期
第三方模块 生态成熟,编译和动态模块都有 需要 xcaddy 自行构建
复杂 rewrite 灵活,文档和例子多 支持,但表达力和资料都少
热加载 先 nginx -t 校验,重载失败靠日志排查 失败自动保留旧配置
团队熟悉度 高,出问题能搜到答案 相对低,排障要靠日志

性能不该是决策依据。常规反向代理场景下,两者都远超业务本身的需求;拿 hello world 的压测数字选型,等于用错误的指标做决定。真正的差异在运维路径:证书谁管、配置谁写、出问题谁查。

我的判断是:单机、少量域名、团队没有专职运维,Caddy 明显省事;已经有成熟的 Nginx 体系、需要复杂规则或特定模块,就不要为了省几行配置去换。

还有一个隐性成本值得算进去:Caddy 的中文资料和问答比 Nginx 少一个数量级。同样的报错,Nginx 能搜到多年前的帖子,Caddy 通常得去读官方文档和 issue。团队里最好有一个人愿意读文档,否则遇到冷门问题会卡很久。

几个真实的坑#

  • 拿不到真实客户端 IP。反代后面拿到的 RemoteAddr 是内网地址。Caddy 会处理 X-Forwarded-For,但要确认上游应用信任这个头,并在 Caddy 侧用 trusted_proxies 限定可信来源,否则这个头可以被伪造。
  • WebSocket 不用额外配置。reverse_proxy 自带协议升级,不需要像 Nginx 那样手动加 Upgrade 头。反过来说,如果你照着 Nginx 的教程加了这些头,可能会出问题。
  • 上传大小要自己设限。反代本身不会替你拦大文件,用 request_body 的 max_size 给个上限,否则任何客户端都能把磁盘写满。
  • 日志默认不落盘。不写 log 指令就只有进程日志,访问日志拿不到,排障时会很被动。
  • 内网环境。没有公网域名时自动签发必然失败,用 tls internal 走本地 CA,或者干脆只监听 HTTP。
  • 权限。非 root 运行又要绑 80/443,需要给二进制加 capability,或者用容器映射端口。
  • 泛域名要换挑战方式。*.example.com 只能走 DNS-01,需要带 provider 插件的构建,并且能读写 DNS 记录。拿不到通配符证书是 ACME 的规则,不是 Caddy 的限制。
  • 上游是 HTTPS 时。反代 https://10.0.0.5:8443 会校验证书,自签证书要显式给出受信 CA,否则表现为 502,日志里只有一句握手失败。
  • 多台机器跑同一域名。默认的本地证书存储各存各的,会各自签发、互相消耗额度。要么把证书存储换成共享后端,要么只在一台机器上终止 TLS。

结论#

  • 单机反代加自动 HTTPS,Caddyfile 三五行就够,先把证书这件事交出去。
  • 上线前逐条核对五个前提,尤其是 /data 持久化和 80 端口可达。
  • 容器里必须挂数据卷,否则重启等于重新签发。
  • 需要第三方模块或复杂 rewrite 时,先评估 xcaddy 自建的维护成本。
  • 日志、真实 IP、上传限制这三项要显式配置,默认值不一定符合你的场景。

完

#Caddy#HTTPS#反向代理#运维
回到顶部 ↑