Caddy 自动 HTTPS 实战:从 Nginx 迁移、Caddyfile 写法、反向代理与五个容易翻车的地方

当 Nginx 的证书配置变成一种负担

我维护着七八个个人站点,全用的是 Let's Encrypt 的免费证书。最开始那几年,证书续期这件事是这样的:每个站点写一份 certbot 的配置,配好 webroot 或者 nginx 插件,然后加一条 cron 定时续期,再用 deploy-hook 重载 Nginx。听起来不复杂,但实际操作起来是这样的——某天你收到一封"证书将在 3 天后过期"的邮件,登录服务器,certbot renew 报个错,原因可能是 ACME 的 challenge 文件没读到、可能是 Nginx 的 server_name 匹配错了、也可能是 DNS 解析变了。然后你花半小时排查,修好,心里想着"下次一定写个监控",然后就没有下次了,直到下一次过期。

我不止一次被证书过期搞到站点变红屏。印象最深的一次是某个周五晚上,一个站点证书过期了,Chrome 直接给出"您的连接不是私密连接"的红色警告页,那个站还在做 SEO,损失可想而知。那次之后我开始认真研究 Caddy。

Caddy 最出名的特点就一句话:自动 HTTPS,零配置。你只要写一个域名,它自己会去申请证书、自己配置 TLS、自己续期,全自动,不需要你操心。对个人站长来说,这个特性直接消灭了上面说的那一整类事故。这篇文章我讲清楚 Caddy 到底怎么用、它和 Nginx 的差异在哪、什么时候该换、什么时候不该换,以及我踩过的坑。

Caddy 凭什么能自动搞定 HTTPS

要理解 Caddy 的自动 HTTPS 为什么和 certbot 不一样,得先看它们的思路差异。

certbot 的思路是"外部工具"。它是一个独立的程序,负责和 Let's Encrypt 打交道,拿到证书文件之后,把文件写到磁盘上(通常是 /etc/letsencrypt/live/<domain>/),然后由你去告诉 Nginx "证书在这里"。为什么需要重载?因为 Nginx 只在启动或者 reload 的时候读取证书文件,运行期间不会重新读。所以整条链路是:certbot 申请 → 写文件 → deploy-hook 通知 Nginx → Nginx reload。中间任何一环出问题,证书就不更新。

Caddy 的思路是"内置"。证书管理是 Caddy 核心功能的一部分,它内部集成了一套完整的 ACME 客户端,还有自己的证书存储(在 ~/.local/share/caddy 或者 /var/lib/caddy 下)。当你写下一个域名的时候,Caddy 会自动:检测这个域名还没有证书 → 发起 ACME challenge → 拿到证书 → 存到自己的存储 → 加载到内存供使用。整个过程中 Caddy 的进程一直活着,它自己在内存里维护证书,不需要外部工具,也不需要 reload。续期也是内置的,它在证书到期前 30 天左右会自动续期,用的是同一套流程。

更关键的一点:Caddy 的**默认配置就是安全的**。如果你只写一个域名,不带任何 TLS 配置,Caddy 会自动使用现代的安全默认(TLS 1.2+、安全的加密套件、OCSP Stapling、HSTS 都需要你显式开启但很多默认就已经不错)。相比之下,Nginx 的默认配置里 SSL 相关的设置几乎都是空的,你得自己一条条查、一条条配,而且很可能配错。这就是所谓的"secure by default"和"secure by configuration"的区别。

再说一个隐藏优势:**Caddy 处理 ACME challenge 的方式更智能**。Let's Encrypt 现在支持三种挑战:HTTP-01、DNS-01、TLS-ALPN-01。Caddy 会**自动选择**它能用的方式。如果 80 端口是通的,它用 HTTP-01;如果只有 443 通,它能用 TLS-ALPN-01(这个很有意思,它不需要 80 端口,直接在 443 的 TLS 握手里完成验证)。而 certbot 传统上主要依赖 HTTP-01,需要你确认 80 端口通。

不过要说明的是,Caddy 的自动 HTTPS 不是"魔法",它建立在几个前提下:域名必须**已经解析到这台服务器**(A 或者 AAAA 记录),并且 80 或者 443 端口**对 Let's Encrypt 的验证服务器可达**。如果域名还没解析,或者防火墙挡了 80/443,它一样会失败。区别在于它的失败日志非常清楚,直接告诉你卡在哪一步。

Caddyfile 语法:比 Nginx 少写一半的行

Caddy 的配置文件叫 Caddyfile,语法风格和 Nginx 差别很大,但一旦上手你会发现它简洁得多。核心思想是:凡是可以推断的地方,你都不用写。

一个最基础的配置长这样:

example.com {
    root * /var/www/example
    file_server
    encode gzip
    log {
        output file /var/log/caddy/example.log
    }
}

就这么几行。对比一下等价的 Nginx 配置:你得写 server 块、listen 443 ssl、listen 80 再 301、写 ssl_certificate 和 ssl_certificate_key 两行指向 certbot 的路径、写 location / try_files、写 gzip 配置、写 access_log。起码二三十行,还要另外跑 certbot。

Caddyfile 里最有用的是**指令(directive)和匹配器(matcher)**的组合。举几个常用的例子。

反代到一个后端服务:

app.example.com {
    reverse_proxy 127.0.0.1:8080
}

注意这里连 upstream 名字、proxy_set_header 那一大堆都不用写。Caddy 的 reverse_proxy 默认就会处理好 X-Forwarded-For、X-Forwarded-Proto、Host 这些头,而且默认支持 WebSocket(Nginx 里你得显式写 Upgrade 头)。这是最让人省心的地方之一。

多个站点共用配置,用 snippets:

(common) {
    encode gzip zstd
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
    }
}

a.example.com {
    import common
    root * /var/www/a
    file_server
}

b.example.com {
    import common
    root * /var/www/b
    file_server
}

这个 (common) 就是一个 snippet,用 import 引入。Nginx 里对应的概念是 include,但 Caddy 的 snippet 能带参数,更灵活。

带路径匹配的路由:

example.com {
    handle /api/* {
        reverse_proxy 127.0.0.1:3000
    }
    handle /static/* {
        root * /var/www/static
        file_server
    }
    handle {
        root * /var/www/html
        file_server
    }
}

这里的 handle 是按顺序匹配的,第一个匹配的生效。注意 handle、handle_path、route 这三个指令的区别:handle 会独占匹配(匹配后不再往下走),handle_path 会在匹配后去掉路径前缀,route 则是按顺序执行不独占。这几个的区别很容易搞混,我的建议是:简单的静态站点用 handle,需要去前缀的(比如把 /api/foo 转成后端的 /foo)用 handle_path,复杂的用 route。

还有一个很多人不知道的功能:**Caddy 可以直接跑反向代理到另一个域名,并且自动处理 TLS**。比如你想做一个内网服务的外网入口:

public.example.com {
    reverse_proxy https://internal.example.com {
        transport http {
            tls
        }
    }
}

它会自动用 TLS 连到后端,连证书验证都帮你处理(如果后端是自签证书,可以加 tls_insecure_skip_verify,但生产环境别这么干)。

Caddy vs Nginx:什么时候该换,什么时候别换

我见过两种极端:一种是把 Caddy 吹成 Nginx 的完美替代品,说"再也不用 Nginx 了";另一种是坚持 Nginx 更强,Caddy 只是玩具。这两种说法都不对。下面是我实际用下来的对比。

Caddy 明显更好的地方:

第一,**证书管理**,这个不用说了,是 Caddy 的杀手锏。维护多个站点的人,省下的时间和心力是巨大的。第二,**配置简洁**,尤其是反代场景,Caddy 的配置量是 Nginx 的三分之一甚至更少,出错的概率也更低。第三,**默认安全**,不用自己去查现代 TLS 配置怎么写。第四,**HTTP/3 开箱即用**。Caddy 默认就支持 QUIC 和 HTTP/3,你什么都不用配(只要 443 UDP 通)。Nginx 要开 HTTP/3 得编译带 QUIC 的版本,还要写一堆配置。第五,**自动 HTTP 到 HTTPS 跳转**,你写了域名,它自己知道要把 http 跳 https。第六,**配置文件可热加载**,改完 `caddy reload` 就生效,不需要想 Nginx 那样担心 reload 时旧连接被断开(虽然 Nginx 的优雅关闭其实也可以)。

Nginx 明显更好的地方:

第一,**性能和成熟度**。在极端高并发的场景下,Nginx 经过十几年的优化,事件模型比 Caddy(Go 语言的 goroutine 模型)在某些基准测试里表现更好。注意定语——"极端高并发",对个人站长的日几万访问量来说,两者差距可以忽略。第二,**生态和资料**。Nginx 的文章、教程、现成的配置片段、Stack Overflow 答案,数量是 Caddy 的几十倍。你遇到问题搜索的时候,Nginx 的答案是现成的,Caddy 的答案可能需要你翻文档。第三,**模块丰富**。Nginx 有大量第三方模块(lua、rtmp、很多安全模块),Caddy 的插件生态虽然也不错(xcaddy 可以自定义构建),但整体数量还少。第四,**很多托管面板和自动化脚本默认只支持 Nginx**。如果你用宝塔、LNMP 一键脚本,换成 Caddy 就得脱离这些工具。第五,**大流量静态资源服务**,Nginx 的 sendfile、zero-copy 优化做得非常成熟。

我的实际选择:静态站点、个人博客、小流量服务、需要多个域名和证书的,我用 Caddy。需要极致性能、需要特定 Nginx 模块、需要跟现有面板集成的,我用 Nginx。还有一种是"混合":前置 Caddy 处理 TLS 和反代,后面 Nginx 处理复杂的 location 逻辑和静态资源加速。这个架构其实挺好用,各取所长。

这里我要特别提一个语法陷阱,很多人从 Nginx 转过来会踩:**Caddy 里 `example.com` 和 `example.com:443` 是有区别的**。前者会同时监听 80 和 443 并自动配置跳转,后者只监听 443,80 端口不会处理。如果你只写 `:443`,用户用 http 访问会得到连接拒绝。另外,如果你只想监听一个非标准端口且不要自动 HTTPS(比如内网场景),要显式写 `http://` 前缀或者 `` 形式,否则 Caddy 会尝试给它申请证书然后失败(因为拿不到公网验证)。

安装与部署:三分钟跑起来

Caddy 的安装非常干净。官方提供了 apt/yum 仓库,也有静态编译好的二进制。官方仓库的方式是最推荐的,因为能自动更新。

在 Debian/Ubuntu 上:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
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 会作为 systemd 服务自启,默认的 Caddyfile 在 /etc/caddy/Caddyfile,服务用的用户是 caddy。这里有一个非常重要的坑:**Caddy 服务默认以 caddy 用户运行,没有 root 权限**。这意味着:

第一,它**不能监听 1024 以下的端口**——不对,这个说法其实不准确。在 Linux 上,非 root 用户监听 80/443 需要权限,但 systemd 的 Caddy 服务文件里已经配置了 ambient capabilities(CAP_NET_BIND_SERVICE),所以它能绑 80/443。这没问题。

第二,它**不能读取你没有给它权限的目录**。/var/www 下的文件如果属主是 root、权限是 644,caddy 用户能读;但如果是 600 或者目录没有 x 权限,就会 403。这个坑非常常见:你迁移过来之后文件都读不到,全是 403 或者 500,原因就是权限。解决办法是确保 caddy 用户对网站目录有读权限,通常是加到 www-data 组,或者用 chown -R 设置合适的属主。

第三,它**写日志的目录必须存在且可写**。你如果配了 `log { output file /var/log/caddy/xxx.log }`,但 /var/log/caddy 不存在,Caddy 启动会失败。所以要提前 mkdir 并 chown caddy:caddy。这是一个启动即失败、但错误信息不一定直观的坑。

另外一个小细节:Caddy 默认会把证书存在 /var/lib/caddy/.local/share/caddy 下(取决于服务用户的 HOME)。如果你想手动管理证书,或者遇到 Let's Encrypt 的申请频率限制(同一域名一周内最多 5 次失败重试),可以换成 ZeroSSL 作为备份 CA(Caddy 支持配置多个 ACME 发行机构,一个失败换下一个)。Caddyfile 里可以这样写:

{
    email you@example.com
    acme_ca https://acme-v02.api.letsencrypt.org/directory
    acme_ca https://acme.zerossl.com/v2/DV90
}

注意第一行的 email 很重要:Let's Encrypt 需要它来发送证书到期提醒(虽然 Caddy 会自己续,但万一续期失败,这是唯一的通知渠道)。不写 email 的话 Caddy 会用一个生成的占位邮箱,你就收不到提醒了。

几个容易翻车的地方

讲了这么多优点,我也得把 Caddy 的坑摊开说,不然你上手之后遇到会骂人。

坑一:DNS 没解析好就别急着启动。Caddy 启动时如果域名解析不到本机,它会尝试申请证书然后失败,然后不断重试。而 Let's Encrypt 对失败的申请有速率限制(每小时 5 次、每周 5 次失败),几次失败之后你可能被锁几小时。稳妥的做法是:**先确认域名 A 记录已经正确解析**(dig +short yourdomain.com 必须返回本机 IP),再启动 Caddy。如果解析还没生效,可以临时用 `tls internal` 生成自签证书先跑起来,等解析生效再改回去。

坑二:Cloudflare 的橙色云会让 HTTP-01 挑战失败。如果你用 Cloudflare 代理(橙色云),所有流量都走 CF,certbot/Caddy 的 HTTP-01 挑战会被 CF 拦截或者返回 CF 的页面,导致验证失败。解决办法有两种:一是申请证书的时候临时把 CF 切成灰色云(DNS only),拿到证书再切回橙色;二是改用 DNS-01 挑战(需要在 Caddyfile 里配置 CF 的 API token 和 DNS 插件)。后者更优雅但需要编译带插件的 Caddy(用 xcaddy)。这个坑我踩过,当时一直以为是防火墙问题,排查了很久。

坑三:Caddy 读取 Caddyfile 的路径取决于启动方式。如果你用 systemd 启动,它读的是 /etc/caddy/Caddyfile。如果你手动执行 `caddy run`,它读的是当前目录下的 Caddyfile。很多人改了 /etc/caddy/Caddyfile 之后手动 caddy run 发现不生效,就是这个原因。所以统一用 `sudo systemctl reload caddy` 来应用改动,不要手动跑。

坑四:reload 之后配置语法错误会导致服务挂掉。改配置时如果写错了语法,`systemctl reload caddy` 会失败,但**旧配置可能也被清掉了**,导致服务不可用。稳妥做法是改完先 `caddy validate --config /etc/caddy/Caddyfile` 检查语法,通过了再 reload。Caddy 的 validate 会告诉你具体哪一行错了,非常好用。

坑五:日志没有轮转会撑爆磁盘。Caddy 默认不会自动轮转日志文件(它的内置 log 不会自动 rotate),你配了 output file 之后,那个文件会无限增长。解决办法是用系统的 logrotate,或者用 Caddy 的 roll_size 和 roll_keep 参数(需要 file 输出配上 roll 选项)。我一开始没注意,某天发现 /var/log/caddy 有几个 G 的日志,磁盘差点满。这个教训要记住:**日志配置一定要配轮转**。

总结:值得为自动化付出的学习成本

回到开头的问题。Caddy 值不值得个人站长从 Nginx 换过来?我的答案是:**如果你被证书续期折磨过,或者你的站点数量多、证书管理是负担,那非常值得**。它的自动 HTTPS 不是噱头,是实打实消灭了一整类运维事故。Caddyfile 的简洁性也让你更不容易写错配置。

但它不是万能替代。如果你追求极致性能、需要特定的 Nginx 模块、或者深度绑定了某个面板,那 Nginx 仍然是更合适的选择。技术选型从来不是"哪个更好",而是"哪个更适合你这个场景"。

我的实际操作路径是:新起的站点、个人项目、需要多域名证书的,用 Caddy;老的、已经在稳定运行的 Nginx 站点,不折腾,保持原样;复杂的大流量站点,用 Caddy 前置 + Nginx 后置的混合架构。这样既能享受自动化的便利,又不用承担迁移的风险。

最后给一个上手建议:不要一上来就迁移生产站点。先在一台测试机上装一个 Caddy,用它跑一个你自己不心疼的小域名,体验一下从零到 HTTPS 只要三行配置的感觉。等你熟悉了 Caddyfile 的语法和那几个坑之后,再考虑把关键站点迁过来。技术迁移最忌讳的就是盲目全量切换,稳扎稳打永远是对的。

Last modification:October 1st, 2026 at 12:25 pm

Leave a Comment