上个月给朋友的博客站点搭服务器,他还是个刚转行前后端的新人。折腾完 Nginx 的配置,他问我:“负载均衡和反向代理我都懂,但为啥我的 Nginx 配置老是一报错就全线崩溃?有没有一个配置少到肉眼能看完、还能自动搞定 HTTPS 证书的服务器?”
我当时脑子里的第一反应是——Caddy,一个定位就是“比 Nginx 更简单”的 Web 服务器。后来我直接把他的静态站从 Nginx 迁到了 Caddy,Caddyfile 总共 8 行,域名证书自动申请,连续期都自己管了,他当场就沉默了。
这篇文章不是要劝你抛弃 Nginx。Nginx 在高并发调优、复杂路由规则上的能力,Caddy 目前还不是一个量级。但如果你经历过“为了 HTTPS 安装 Certbot、配定时任务、写长串配置”这种折磨,那 Caddy 真的很值得重新认识。这篇文章就围绕 Caddy 和 Nginx 的核心差异,把“为什么 Caddy 更简单、简单在哪里、又在哪里坑过你”一次讲清楚。
1. 为什么说 Caddy 比 Nginx 更简单:先搞清楚差异在哪里
不少人的第一反应是“Caddy 是 Go 写的,性能是不是不行?”。说实话,如果不是追求单机百万并发,这种担心大多是多虑。Caddy 的目标用户压根不是那些需要翻着官方文档调几百个 worker 参数的团队,而是那些就想快速、安全、省心地跑起来一个服务的开发者和中小型项目。
1.1 配置哲学的根本区别
Nginx 是一个“声明式”配置系统。你要告诉它:监听哪个端口、虚拟主机指向哪个目录、location 匹配规则是什么、反向代理往哪里转发、缓存开多大、gzip 开不开。每一条都需要你明确地写出来,而且它有自己一套复杂的继承和作用域逻辑。
Caddy 是“意图式”配置。你只要告诉它“我要把这个域名跑起来,根目录是 /var/www”,剩下的监听、日志、静态资源处理、HTTP/2、甚至 HTTPS 证书,它自己就全办了。
# Nginx 要跑一个最简单的静态站点,光基础配置就至少 20 行 server { listen 80; server_name example.com; root /var/www/example; index index.html; location / { try_files $uri $uri/ =404; } }# 同样的事情,Caddy 只需要 3 行 example.com { root * /var/www/example file_server }从“告诉它每一步怎么走”到“告诉它结果是什么”,这就是两者配置哲学的分水岭。这也是 Caddy 被很多老运维称为“傻瓜但聪明”的核心原因。
1.2 HTTPS 自动化:Caddy 最大的降维打击
在 Nginx 上启用 HTTPS,你要经历:买证书或配置 certbot → 设置 challenge 方式 → 编辑 nginx.conf 指向证书位置 → 配置自动续期 cron 任务 → 确认续期脚本执行成功。任何一个环节出岔子,网站的证书就会悄悄过期,然后浏览器弹出红色警告。
Caddy 的做法激进到你有点怀疑人生:只要站点地址是域名,并且服务器能正常访问 80/443 端口,它默认就自动完成 HTTPS。
背后原理其实不玄乎。Caddy 内部内置了 ACME 客户端,启动时检测到域名配置,就会自动向证书颁发机构申请证书,再把证书写到内部存储,同时开启 HTTPS 监听。到了快过期的时间,它又自动发起续期请求,全程不需要额外进程、不需要 cron、不需要 reload。
注意:Caddy 这个自动 HTTPS 的强制性很强。你如果在 Caddyfile 里写了一个域名,但忘了把域名解析到服务器 IP,它启动时证书申请会失败,站点会直接无法访问,并给出明确的错误日志。这会让你被迫去把 DNS 配好,而不是像 Nginx 那样 HTTP 还能先开着。
1.3 面向场景的设计取舍
Nginx 是“通用型服务器”,它既能做静态服务、反向代理,也能做 TCP/UDP 负载均衡,还自带丰富的 njs 脚本扩展。但这一切强大的背后,是学习曲线和配置复杂度的急剧上涨。
Caddy 偏“场景化”,它的核心目标很聚焦:让普通站点、API 服务的部署体验变得极度顺滑。它的模块化架构也基于 Go 插件机制,比如 file_server、reverse_proxy、basicauth 都是插件。好处是想要的功能只要在 Caddyfile 里写一行就能启用,坏处是调试时你得知道这些插件在背后做了什么。
我给一个很实际的判断标准:
| 场景 | 推荐选择 | 原因 |
|---|---|---|
| 个人博客、静态站点、工具站 | Caddy | 配置少、HTTPS 自动、维护成本极低 |
| 小型 API 服务、微服务网关 | Caddy | 反向代理配置简单,自动处理 WebSocket、HTTP/2 |
| 大型站点、复杂 URL 重写、多级缓存 | Nginx | 生态成熟、调优空间大、社区方案丰富 |
| 负载均衡、高并发场景 | Nginx | 事件驱动模型成熟,性能和并发能力经过海量验证 |
从 0 到 1 快速起一个服务,Caddy 的体验真的能带来那种“原来配服务器也能这么快”的爽感。但如果你是要在已有的大规模架构里做深度定制,Nginx 的灵活性和生态依旧不可替代。
2. 上手实操:十分钟把 Nginx 翻译成 Caddyfile
我底下结合自己迁移博客站、前端项目部署的经验,对照着展示 Nginx 配置翻译成 Caddy 配置的过程。建议你跟着敲一遍,Caddyfile 的语法体系虽然简洁,但细节上还是有几个地方值得记一下。
2.1 最基础的静态站点配置
Nginx 的静态站点配置往往要考虑到 root 路径、index 文件、try_files 规则、日志路径、gzip 开关等。Caddy 则把这些“最佳实践”直接内置了。
# Nginx 静态站点(带 gzip 和缓存调整) server { listen 80; server_name example.com; root /var/www/example; index index.html; location / { try_files $uri $uri/ =404; } location ~* \.(js|css|png|jpg|jpeg|webp|svg)$ { expires 30d; add_header Cache-Control "public, no-transform"; } gzip on; gzip_types text/plain text/css application/javascript application/json image/svg+xml; }Caddy 版:
# Caddy 静态站点(自带 gzip、缓存控制、日志) example.com { root * /var/www/example encode gzip file_server }注意root * /var/www/example里的*,它表示匹配所有路径,告诉 Caddy 这个 root 适用于所有请求。如果不写*,Caddy 的 root 指令默认只作用于精确匹配的路径,新手很容易在这里踩坑,导致图片、CSS 加载不出来。
Caddy 的file_server会自动处理 index.html、目录浏览(默认关闭,也可以用browse参数开启)、智能 MIME 类型识别等。encode gzip一行就搞定了 Nginx 里一大段的 gzip 配置。
2.2 反向代理配置:一行搞定
反向代理是 Nginx 用得最多的场景之一。传统写法是:
# Nginx 反向代理到 http://localhost:8080 server { listen 80; server_name api.example.com; location / { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }Caddy 只需要:
# Caddy 反向代理到 http://localhost:8080 api.example.com { reverse_proxy localhost:8080 }Caddy 在自动做反向代理时,会默认补全 Host 头、X-Forwarded-* 头、WebSocket 支持。Nginx 需要手动指定的那些 header,Caddy 内部都处理好了。
这里有个特别爽的点:Caddy 对后端服务加健康检查也极其简单。比如后端 API 挂了,Caddy 可以自动把请求转发到备用节点:
api.example.com { reverse_proxy { to localhost:8080 to localhost:8081 health_uri /health health_interval 30s health_timeout 5s } }Nginx 做类似的事需要在 upstream 块中配置 health_check,并且还要依赖商业版本或者第三方模块,Caddy 这几行配置就把高可用基础能力给落地了。
2.3 路由与路径匹配规则
Caddy 的路由匹配语法和 Nginx 的 location 有相似之处,但更直接。最常用的几种匹配方式:
example.com/foo:精确匹配路径 /fooexample.com/foo/*:匹配 /foo/ 下所有路径example.com/*.css:匹配所有 .css 结尾的文件example.com/api/*:匹配 /api/ 前缀的所有路径
比如你要实现这样的 Nginx 效果:
location /api/ { proxy_pass http://backend:8080; } location /static/ { alias /var/www/static/; }Caddy 对应写法:
example.com { handle /api/* { reverse_proxy backend:8080 } handle /static/* { root * /var/www/static file_server } handle { root * /var/www/example file_server } }handle块有点像 Nginx 的 location,但它按书写顺序匹配,并且只执行第一个匹配到的块。这个“顺序即逻辑”的规则,让配置在排错时更直观,不用像 Nginx 那样还要考虑 location 的匹配优先级算法。
提示:Caddy 里还有
handle_path指令,它会自动去掉匹配到的路径前缀后再传给后端。比如handle_path /api/* { reverse_proxy localhost:8080 },请求 /api/users 转发后会变成 /users。Nginx 里你需要自己去配 rewrite 规则才能实现相同效果。
3. 比 Nginx 更省心的生产级配置:从安全到性能的核心细节
Caddy 的简单,核心是“把复杂留给内部”,而不是“省掉应有的安全机制”。我自己在玩 Caddy 的过程中,最深的体会就是它帮我把 HTTPS 和请求头管理的门槛大幅降低了,但真要上生产,还是有几个细节值得逐一梳理。
3.1 自动 HTTPS 的完整链路:申请、续期、部署
Caddy 的自动 HTTPS 并不仅仅是在启动时去申请一次证书就完事,它背后是一整套生命周期管理机制。
- 申请:当 Caddyfile 中的站点地址是域名且未配置
tls指令禁用 HTTPS 时,Caddy 自动使用内置的 ACME 客户端向 Let's Encrypt(默认)或 ZeroSSL(可配置)申请证书。 - 验证:默认使用 HTTP-01 challenge。Caddy 会在 80 端口临时响应证书颁发机构的验证请求,所以你需要确保服务器的 80 端口对公网可访问。
- 存储:证书和私钥存放在 Caddy 的数据目录(默认
/var/lib/caddy/)。升级 Caddy 或迁移服务器时,备份这个目录比备份配置文件还重要。 - 续期:Caddy 在证书过期前 30 天左右,会自动检查并触发续期。续期后自动重载证书,完全无需重启进程。
这里有个比较实用的细节:如果你用 Caddy 内部存储的证书,想手动查看证书信息,可以在服务器上执行:
caddy cert list想强制续期,可以执行:
caddy renew --force这些命令式的操作,在 Nginx 生态里通常要用certbot renew --force-renewal之类的外部命令,而且 certbot 还要额外处理 nginx 配置的联动,Caddy 则把这些全部收敛到一起了。
3.2 安全相关配置:header、权限、限制
Caddy 让安全配置变得极其直白。比如给站点统一加安全响应头:
example.com { header { X-Content-Type-Options nosniff X-Frame-Options SAMEORIGIN Referrer-Policy no-referrer } }Nginx 里加这些 header 时,要格外留意是否会影响静态资源加载、有没有被后端覆盖等,调试起来经常一脸懵。Caddy 的header指令默认会自动处理掉一些多余的默认头,比如Server头,这对减少信息暴露很有帮助。
请求体大小限制:
example.com { request_body { max_size 10MB } }限制某些路径访问(比如屏蔽后台登录接口):
example.com { @blocked { path /admin/* remote_ip 203.0.113.0/24 } respond @blocked 403 }这种“先定义匹配条件,再指定动作”的 Caddy 语法,和 Nginx 的if + return 403相比,语义上更容易读懂,也不会踩到 Nginx 里if指令的经典深坑。
注意:Caddy 在写这类“条件阻断”的时候,一定要把
@blocked这个命名匹配定义在respond之前。Caddyfile 的解析顺序是从上到下,一旦某个handle块先匹配并返回了内容,后续规则就不会执行了。
3.3 性能调优与观测:日志、metrics、优雅重启
很多人误以为 Caddy 简单就等于没有性能可调。其实它只是把大部分默认参数调到了很合理的值,但真正用起来,从日志和监控上能拿到的东西一点不少。
访问日志默认是结构化 JSON 格式。想要输出到文件,可以这样配置:
example.com { log { output file /var/log/caddy/example.log { roll_size 100MB roll_keep 5 } format json } }日志按时间自动滚动、自动压缩,这个内置的轮转能力,比起 Nginx 里要自己写 logrotate 配置方便不少。
Caddy 自带一个管理接口(默认监听 localhost:2019),你可以通过它实时查看配置、修改配置、甚至热加载新配置。这在生产环境里非常实用:
curl localhost:2019/config/如果要给 Prometheus 之类监控系统暴露指标,Caddy 有内置的 metrics 端点,只需要在全局配置中启用servers { metrics }:
{ servers { metrics } } example.com { root * /var/www/example file_server }访问localhost:2019/metrics就能看到请求数、响应状态码分布、延迟等指标。这一点比 Nginx 开源版强很多,Nginx 开源版要 metrics 往往要借助nginx-module-vts或prometheus-nginx-exporter这类额外组件。
Caddy 的热重载和无损重启体验也很舒服。修改 Caddyfile 后执行:
caddy reload --config /etc/caddy/Caddyfile它会平滑加载新配置,已建立的连接不会断,不会出现 Nginx 那种改完配置先nginx -t再nginx -s reload的两步操作。
4. 生产环境踩坑记录:我实际遇到过的 5 个典型问题
Caddy 虽然简单,但简单不等于没有坑。下面这些是我实际部署过程里遇到的问题,逐一记录下来,希望能帮你少走点弯路。
4.1 端口占用与 systemd 启动失败
Caddy 默认监听 80 和 443 端口。如果服务器上之前装过 Nginx、Apache 或别的服务占用了这两个端口,Caddy 启动时会直接报错退出。
我遇到过一次,服务器上原本的 Nginx 没有卸载干净,Caddy 启动后报listen tcp :443: bind: address already in use。排查方式:
ss -tlnp | grep ':80\|:443'找到占用进程后,确认是残留的 Nginx:
systemctl stop nginx systemctl disable nginx然后重启 Caddy。如果你是用 systemd 管理 Caddy,启动失败时先查看日志:
journalctl -u caddy -n 100 --no-pagerbind: address already in use和permission denied是最常见的两类启动错误。后者多半是没给 Caddy 进程分配 80/443 的绑定权限,检查一下 systemd 的AmbientCapabilities配置或直接使用 root 权限启动(不推荐,但常见)。
4.2 域名解析与证书申请失败的排查
Caddy 自动申请证书失败是新手最容易懵的问题。报错信息通常是:
error: obtaining certificate: acme: error: 404 - urn:ietf:params:acme:error:unauthorized这个 404 一般是验证路径无法访问导致的。常见原因:
- 域名解析还没生效,ACME 服务器访问不到你的服务器 IP。
- 防火墙把 80 端口挡了。HTTP-01 验证必须能通过公网访问 80 端口的
/.well-known/acme-challenge/路径。 - 云服务商安全组没放行 80 端口。
排查顺序很重要:先dig +short example.com确认解析地址,再本地浏览器访问http://example.com/.well-known/acme-challenge/xxx看是否 404,最后检查安全组和本机防火墙。
提示:Caddy 在证书申请失败时会自动重试,默认间隔是 30 秒左右。如果你在短时间内连续改错配置,触发太多次失败,可能被证书机构临时拉黑,这时最好的策略是等几分钟再试,而不是疯狂重启服务。
4.3 HTTP/3 与 UDP 端口的坑
Caddy 从 2.4 版本开始默认支持 HTTP/3(基于 QUIC,使用 UDP 443端口)。这个默认开启的行为,在给大多数项目用的时候没啥问题,但如果你的网络环境(比如某些云主机安全组)只放行了 TCP 443,忘了放行 UDP 443,那么 Caddy 的 HTTP/3 会一直报错或接收不到数据。
我踩过的一次坑是:前端同事反馈某些网络环境访问站点时图片加载特别慢,排查一圈发现是 UDP 443 被防火墙丢弃,导致 HTTP/3 回退到 HTTP/2 时出现延迟。解决办法很简单:在云控制台安全组里把 UDP 443 也放行,或者干脆在 Caddyfile 里关闭 HTTP/3:
{ servers { protocols h1 h2 } }我不建议一上来就关掉 HTTP/3。现在移动网络环境对 QUIC 的支持已经很好,打开它确实能带来首屏性能提升。但你要做好端口放行的配置,否则会出现“时好时坏”的玄学现象。
4.4 反向代理时 WebSocket 连接不稳
Caddy 的reverse_proxy对 WebSocket 是自动支持的,也不需要像 Nginx 那样手动配置Upgrade、Connection头。但这不代表没有坑。
我部署过一个在线聊天服务,前端通过 WebSocket 连接 Caddy 再转发到 Node.js 后端口,连接总是几秒钟后断开。排查下来,问题出在后端服务的心跳周期和 Caddy 的默认超时上。Caddy 的reverse_proxy默认有 60 秒左右的无活动超时,如果业务心跳间隔超过这个值,连接会被 Caddy 主动掐断。
解决方案是给反向代理配置更长的超时时间:
example.com { reverse_proxy localhost:3000 { health_timeout 10s transport http { read_timeout 300s write_timeout 300s } } }还有一个容易忽略的点:如果 WebSocket 后端需要拿到用户真实 IP,需要在反向代理中这样配置,让 Caddy 把客户端地址传给后端:
example.com { reverse_proxy localhost:3000 { header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} } }4.5 与 Docker 容器联动时的 network 模式选择
Caddy 最常见的部署场景之一就是给 Docker 容器做反向代理。如果你是使用 docker-compose 部署,需要注意 Caddy 所在容器和业务容器需要在同一个 Docker 网络里,才能用容器名互相访问。
我的建议是 Caddy 使用 host 网络模式,这样它可以直接监听宿主机 80/443 端口,访问后面容器时用localhost:8080形式。这在小型项目和家庭服务器中操作最省心,尤其是当你不想处理 Docker 默认 bridge 网络的端口映射时。
services: caddy: image: caddy:latest container_name: caddy restart: unless-stopped network_mode: host ports: - target: 80 published: 80 protocol: tcp mode: host - target: 443 published: 443 protocol: tcp mode: host - target: 443 published: 443 protocol: udp mode: host volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data - caddy_config:/config不过要注意,host 网络模式下你无法再用localhost访问 Caddy 的 admin API 来做动态配置(默认管理接口只监听容器的 loopback)。如果要用 Docker 方式做更精细的网络隔离,就用 compose 自建网络并让 Caddy 加入业务网络,用服务名作为 upstream 地址。
另外,caddy_data这个 volume 一定要挂出来。证书、私钥、ACME 账号信息都存在里面。如果不挂载,容器被删除后证书会重新签发,虽然能自动完成,但会浪费 ACME 的每周申请限额,而且账号信息丢失可能导致后续的续期出现问题。
写在最后:什么场景我依旧不推荐 Caddy?
我在这篇文章前半部分夸了 Caddy 很多,但作为用 Nginx 超过八年的老用户,我得客观说几句。Caddy 不适合哪些场景?根据我的实际经验:
- 你需要对服务器进行极其细致的调参,比如调整 worker 连接数、epoll 事件处理策略、sendfile 相关参数等,这类底层优化 Nginx 的社区经验更丰富、更成熟。
- 你有大量历史遗留的 Nginx 配置和团队习惯,强行迁移到 Caddy 带来的学习成本和迁移成本,可能大于它带来的部署便利。
- 你的项目依赖 OpenResty 或 lua-nginx-module 这类 Nginx 生态插件。
但如果你做一个独立站、一个内部系统、一个小型 SaaS 的网关,我建议你真的可以花半小时试试 Caddy。至少,以后你再也不用在半夜收到“证书过期”的短信了。
最后分享一个我个人的操作习惯:在服务器上始终保留一份 Nginx 和 Caddy 共存的方案。Nginx 监听 80 端口做统一入口,把特定路径转发给 Caddy(比如/caddy/走 Caddy),这样既能享受 Caddy 的自动 HTTPS,又能保留 Nginx 在流量入口的控制力。这种“混搭”架构看起来有点怪,但实际运行起来非常稳,也让我对两者的定位有了更深的对比理解。