为什么要考虑从 Nginx 换到 Caddy
做了几年站的朋友,手里多半都有一套 Nginx 配置。它稳定、性能好、资料多,几乎是小服务器的默认选择。但 Nginx 有一个长期被诟病的地方:HTTPS 证书要自己申请、自己续期、自己 reload,稍不注意就过期了,浏览器一片红色警告,用户直接跑光。以前我们靠 acme.sh 或 certbot 配 cron 兜底,能跑,但配置分散在两三个地方,排查起来费神。
Caddy 这个用 Go 写的 Web 服务器,最大的卖点就是「默认自动 HTTPS」。你只写一个域名,它会自己去 Let's Encrypt 申请证书、自动续期、自动重定向 80 到 443,全程零额外配置。这对个人站长来说,等于把最容易翻车的一环直接接管了。这篇文章不讲虚的,从零把 Caddy 2 装起来,配一套真实可用的反向代理,并说清楚它和 Nginx 的取舍。
安装 Caddy 2:官方源与二进制两种方式
Debian/Ubuntu 上最省事的是用官方 apt 源,这样以后 apt upgrade 就能跟着升级:
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 -y caddy
装完 Caddy 会注册一个 systemd 服务,但注意:默认的 /etc/caddy/Caddyfile 只是一个 hello world。查看状态用 systemctl status caddy,查看配置校验用 caddy validate --config /etc/caddy/Caddyfile。
如果你不想被 apt 源绑定,也可以直接下载静态二进制,放进 /usr/local/bin,然后自己写一个 systemd unit。对个人站来说,apt 版更省心,我推荐 apt。
Caddyfile 语法:比 Nginx 少一半的心理负担
Caddy 的主配置叫 Caddyfile,结构非常直观:第一行是站点地址(域名),后面大括号里是各种指令。下面是一个最小但完整的反代示例,把 example.com 的请求转发到本机 3000 端口的应用:
example.com {
encode gzip zstd
reverse_proxy 127.0.0.1:3000
}
就这么四行,它已经帮你做完了:申请证书、开启 HTTPS、把 http 跳转到 https、对响应做 gzip/zstd 压缩、把请求转发给后端。对应的 Nginx 配置至少要写一个 80 server 块跳转、一个 443 server 块、ssl_certificate 两行、proxy_pass 若干行,加起来二三十行,而且证书还得单独管。
Caddy 里 encode 就是开启压缩,reverse_proxy 就是反代,file_server 就是静态文件服务,root 指定根目录,语义几乎和英文单词一致。这就是它「配置少、心智负担低」的来源。
多站点与子域名:一个文件管所有
Caddyfile 里可以并列写多个站点块,Caddy 会自动为每个域名单独申请证书:
example.com {
root * /var/www/example
file_server
encode gzip zstd
}
blog.example.com {
reverse_proxy 127.0.0.1:8080
}
api.example.com {
reverse_proxy 127.0.0.1:9000
}
注意 root * 里的 * 是匹配器(matcher),表示匹配所有请求路径。Caddy 的匹配器比起 Nginx 的 location 正则更接近自然语言,常见的有 path、host、header、method,多个条件用逗号连接表示「或」,用空格或嵌套表示「与」。
反向代理的实战细节:头、超时、后端健康
反代最容易出问题的不是转发本身,而是请求头和超时。Caddy 的 reverse_proxy 默认会自动设置 X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Host,并传递 Host。如果你的后端应用需要知道真实客户端 IP,一般不用额外配置。但要给后端换一个 Host 头(比如后端是 WordPress,需要识别域名),可以这样写:
reverse_proxy 127.0.0.1:8080 {
header_up Host {upstream_hostport}
header_up X-Real-IP {remote_host}
}
超时方面,Caddy 默认没有硬性读超时,但如果你跑的是长连接(如 WebSocket 或 SSE),要确保 reverse_proxy 支持流式。Caddy 默认就支持 WebSocket 透传,不需要像 Nginx 那样手动加 Upgrade 头——这是又一个省心点。
后端如果有多台,可以写成负载均衡:
reverse_proxy 127.0.0.1:8080 127.0.0.1:8081 {
lb_policy least_conn
health_uri /healthz
health_interval 10s
}
health_uri 让 Caddy 主动探测后端存活,挂了就摘除,恢复就加回,这对个人站跑两个进程做主备非常实用。
静态站点的缓存与安全响应头
静态站最在意缓存。Caddy 的 header 指令配合匹配器,可以给不同类型资源设置不同的 Cache-Control:
example.com {
root * /var/www/example
file_server
@static path *.css *.js *.jpg *.png *.woff2
header @static Cache-Control "public, max-age=31536000, immutable"
@html path *.html
header @html Cache-Control "public, max-age=300"
header {
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}
}
带哈希指纹的静态资源用一年 immutable,HTML 用短缓存,这是性能和安全的标准打法。注意 Caddy 里 header 不带参数时是「设置」语义,多次出现会累加不同字段,很方便集中管理安全头。
用 Caddy 内置的限流与访问控制
Caddy 核心不含限流模块,但官方维护的插件生态很成熟,比如 caddy-ratelimit。要自定义构建带插件的 Caddy,用官方推荐的 xcaddy:
xcaddy build --with github.com/mholt/caddy-ratelimit
构建出来的二进制替换原来的 /usr/bin/caddy 即可。这比给 Nginx 编译第三方模块要干净得多,Go 的编译是自包含的,不会污染系统库。升级时重新 build 一次就行。当然,如果你不想折腾,直接在前面套一层 Cloudflare 免费 CDN 做 WAF 和限速,也是常见组合。
Caddy 与 Nginx 的取舍:到底该不该换
说句公道话,两者不是谁替代谁。Caddy 的优势在于:自动 HTTPS、配置简洁、内置 HTTP/2 与 HTTP/3、WebSocket 透传零配置、Go 单文件部署。它的弱点是:极端高并发下的极限性能略逊于 Nginx(但对个人站根本摸不到这个天花板)、资料和中文教程相对少、某些冷门模块要自己 build。
我的建议是:如果你站点的配置主要是「反代 + 静态 + 自动 HTTPS」,Caddy 能让你少维护一大半东西,值得换;如果你的站点深度依赖 Nginx 的 Lua、map 复杂分流、或者已经有一套跑得很稳的优化配置,那就别为了新而新,继续用 Nginx。技术选型的标准永远是「省不省心、稳不稳」,而不是「新不新」。
迁移时最容易踩的三个坑
第一,端口占用。Caddy 要独占 80 和 443,迁移前务必先 systemctl stop nginx 并 disable,否则 Caddy 起不来还会报 bind 失败。第二,DNS。Caddy 申请证书依赖域名 A 记录已经指向本机,如果域名还在解析切换中,证书签发会失败,建议先把 DNS 处理好再启 Caddy。第三,日志与数据目录。Caddy 的证书、密钥、账户信息都放在 /var/lib/caddy,日志在 journald,别把这两个目录挂到会被清理的临时盘上。
排查证书问题最有用的一条命令是看日志:journalctl -u caddy -f,ACME 的失败原因都会明确写出来,比如 DNS 校验失败、或者 80 端口被挡。搞清楚原因后对症处理,比盲目重试快得多。
证书自动续期背后的机制与注意点
很多人以为「自动 HTTPS」是黑魔法,其实它的原理并不复杂。Caddy 启动时检查每个站点块里的域名,如果 /var/lib/caddy/.local/share/caddy(或系统版的 /var/lib/caddy)里没有该域名有效且未过期的证书,它就会走 ACME 协议向 Let's Encrypt 申请。申请的第一步是「域名控制权验证」,Caddy 默认优先用 TLS-ALPN-01(在 443 端口上用特殊 TLS 握手证明你是域名主人),失败则回落到 HTTP-01(在 80 端口放一个临时文件)。
这就解释了几个常见现象。第一,如果你机器上的 80 或 443 被别的进程占用,或者云厂商安全组没放行这两个端口,证书申请会失败,日志里会写 challenge failed。第二,如果域名解析还没生效(A 记录还指向旧 IP),同样会失败,因为 CA 校验时访问的是那个旧 IP。第三,申请频率有限制——Let's Encrypt 对同一域名每周有签发上限,所以别反复重启 Caddy 去「催」证书,反而可能触发限流。
续期是自动的:Caddy 会在证书到期前约 30 天自动重新申请,无需 cron。这套机制的好处是彻底消灭了「证书过期」这个老大难。你唯一需要留意的是保持 Caddy 进程常驻(systemd 已帮你做到),以及别把证书目录所在磁盘写满——磁盘满了续期会静默失败,这是很阴的一个坑。
日志格式与访问日志落盘
Caddy 默认把访问日志打到标准输出,也就进了 journald,用 journalctl -u caddy 看。但要做访问分析或排查爬虫,最好把日志单独落盘。可以在站点块里配 log 指令:
example.com {
log {
output file /var/log/caddy/example.log {
roll_size 50mb
roll_keep 10
}
format json
}
reverse_proxy 127.0.0.1:3000
}
Caddy 会自动做日志轮转(按大小 50MB 切割、保留 10 份),不需要额外配 logrotate,这一点比 Nginx 省事。format json 输出的结构化日志用 jq 处理很方便,比如统计某 IP 的请求数、找出被返回 404 最多的路径:
jq -r 'select(.status==404) | .request.uri' /var/log/caddy/example.log | sort | uniq -c | sort -rn | head
这种结构化日志,对排查死链、分析爬虫行为、定位刷流量的恶意 IP 都非常有用,是运维的常规武器。
小结
Caddy 2 用「默认自动 HTTPS」这一个特性,就足以让大量个人站长重新考虑 Web 服务器的选择。它把证书这个最容易过期、最容易被忽略的环节彻底接管,配置语法也把「反代 + 静态 + 压缩 + 安全头」压缩到十几行。对一台小 VPS 来说,少维护一个环节,就少一次半夜救火。Nginx 依然是老兵,但如果你受够了管证书,Caddy 值得你花一个下午试试。