2026-09-24 更新:第 6 节「错误页面处理」补了一段实测教训 —— 拿静态文件当错误页,会跟着站点改版慢慢跑偏;我们把错误页改成了 PHP 渲染、和别的页面共用同一份模板。
前段时间我把万象存笺从 Nginx 切到了 Caddy,整个过程踩了不少坑,也切实感受到了两个服务器在设计理念上的差异。这篇文章做个完整对比,如果你是个人站长或小项目运维者,可以作为选型参考。
为什么会有这次切换
万象存笺原本跑在 Nginx 上,前面有 EdgeOne CDN 做 HTTPS 终止和缓存加速,Nginx 主要承担 PHP-FPM 转发、静态文件服务和安全策略(防盗链、敏感文件屏蔽、安全头)这些"后端活"。
在 CDN 架构下,Nginx 的很多能力其实用不上:
- GeoIP 国家封锁:请求来源全是 CDN 回源 IP,不是真实用户 IP,检测无效
- 限速(rate_limit):同理,$binary_remote_addr 拿到的也是 CDN 节点 IP
- 复杂的 location 正则:配置越复杂,后续维护成本越高
既然 Nginx 的优势发挥不出来,而 Caddy 又以"配置简洁、自动 HTTPS"闻名,就动了切换的念头。
配置对比:从 Nginx 到 Caddyfile
先看实际配置的体量差距。
Nginx 配置(仅核心站点)
server {
listen 443 ssl http2;
server_name 3z.ink www.3z.ink;
ssl_certificate /etc/nginx/ssl/markdown-cms.crt;
ssl_certificate_key /etc/nginx/ssl/markdown-cms.key;
root /var/www/markdown-cms/public;
index index.html;
# 安全头
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
# ... 一堆 header
# PHP 转发
location ~ ^/api/[a-z-]+\.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}
# 防盗链
location ~* \.(png|jpg|jpeg)$ {
valid_referers none blocked www.3z.ink;
if ($invalid_referer) { return 403; }
}
# 敏感文件屏蔽
location ~ /\. { deny all; }
location ~ \.(md|env|git|sql)$ { deny all; }
# 错误页面
error_page 404 /404.html;
error_page 403 /403.html;
}这只是精简版。实际完整的 Nginx 站点配置大约 130 行,加上全局配置还要更多。
Caddyfile 配置(等价功能)
3z.ink, www.3z.ink {
encode gzip
tls internal
root * /var/www/markdown-cms/public
# 安全头
header {
Strict-Transport-Security "max-age=63072000"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
# ... 其他 header
}
# PHP 处理(一行搞定)
php_fastcgi unix//var/run/php/php8.3-fpm.sock
# 防盗链
@hotlink-images {
path *.ico *.jpg *.jpeg *.png *.gif *.svg *.webp *.bmp
not {
header_regexp referrer-ok Referer "^https?://(www\.)?123456l\.com($|/)"
}
}
respond @hotlink-images 403
# HTML 无后缀访问
handle {
try_files {path} {path}.html {path}/
file_server
}
# 错误页面
handle_errors {
@404 { expression {http.error.status_code} == 404 }
rewrite @404 /404.html
file_server
}
log {
output file /var/log/caddy/access.log
}
}Caddyfile 大约 60 行,同样的功能,配置量减半。关键是语法更直观——没有 location 嵌套、没有 if 条件判断、没有 upstream 配置。
核心差异对比
1. 配置语言
| 维度 | Nginx | Caddy |
|---|---|---|
| 语法风格 | 声明式 + 指令式混合 | 极简声明式 |
| 学习曲线 | 陡峭(location 优先级、rewrite 规则) | 平缓(无嵌套、无优先级问题) |
| 配置量 | 同功能多 2~3 倍 | 行数少一半 |
| 热重载 | nginx -s reload 支持好 |
systemctl reload caddy 可能有问题,推荐 restart |
Caddy 的 Caddyfile 语法最舒服的一点是不需要记指令优先级。Nginx 的 location 块有复杂的匹配优先级(精确匹配 > 前缀匹配 > 正则匹配 > 通用匹配),写错顺序就出 bug。Caddy 用 route {} 块让指令按书写顺序执行,清晰很多。
2. SSL/TLS 证书管理
Caddy 最出名的特性就是自动 HTTPS。配置里写 tls internal 就能自动生成自签证书(供 CDN 回源用),写 tls your@email.com 就能自动申请 Let's Encrypt 证书。
Nginx 需要自己管理证书:手动申请、配置续期 cron、指定证书路径。虽然 Certbot 可以自动化,但毕竟是额外工具。
对于前端有 CDN 的架构,这点差异特别明显——服务端只需要一个自签证书供 CDN 回源验证,Caddy 一行 tls internal 搞定,每 12 小时自动轮换。
3. PHP-FPM 集成
Nginx 处理 PHP 需要:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}Caddy 一行:
php_fastcgi unix//var/run/php/php8.3-fpm.sock而且 Caddy 内置了对 PATH_INFO 的支持,不需要额外配置。不过要注意,Caddy 的 php_fastcgi 必须和 handle 块配合好路由顺序,否则 PHP 文件可能被当作静态文件直接返回(我踩过这个坑,后面会讲)。
4. 性能
| 维度 | Nginx | Caddy |
|---|---|---|
| 静态文件 | 极优(epoll 事件模型) | 优秀(同样 Go net/http) |
| 反向代理 | 极优(内存占用极低) | 良好 |
| 并发连接 | 百万级(业界标杆) | 万级(Go goroutine 模型) |
| TLS 性能 | 良好(OpenSSL) | 优秀(Go 原生 crypto/tls) |
| 内存占用 | ~10MB(空闲) | ~15-20MB(空闲) |
说实话,在个人站点场景下,性能差异可以忽略不计。万象存笺日均 PV 不到 1000,Caddy 和 Nginx 都跑不满 1% 的 CPU。
Caddy 用 Go 写的,内存占用确实比 Nginx 高一些(Nginx 在 C 层面做得极轻量),但多出来的 10MB 内存对于现在 2GB+ 的云服务器来说完全不是问题。
5. 生态与模块
Nginx 的生态非常成熟:有 Lua 扩展(OpenResty)、有丰富的第三方模块、有成熟的运维工具链(Prometheus exporter、访问日志分析器)。
Caddy 的模块系统也很强大,但生态没有 Nginx 丰富。Caddy 的插件是用 Go 编译的,意味着加一个模块就得重新编译整个 Caddy 二进制。不过对于个人站点,Caddy 内置的功能已经足够了。
6. 错误页面处理
Nginx 用 error_page 指令指定错误页:
error_page 404 /404.html;
location = /404.html { internal; }Caddy 用 handle_errors 块:
handle_errors {
@404 { expression {http.error.status_code} == 404 }
rewrite @404 /404.html
file_server
}Caddy 的方式更灵活——可以在错误处理块里做条件判断、重写、甚至执行 PHP。
上面两种写法有个共同前提:错误页是独立文件。这个前提在小站上容易埋雷 —— 站点改版时,404.html / 403.html 里手写的那份导航栏和页脚不会跟着模板走,得挨个同步。我们这边就是这么踩的:站点改版几次下来,404 页跟上了模板,403 页的导航还停在四个旧条目的版本,手机上栅格也少一档,直到某次改页脚才发现两张错误页长得不一样。
后来干脆把错误页改成 PHP 渲染:页头、导航、页脚 require 同一份模板文件,改一次全站生效;403/404 的文案单独抽成一个共用函数,兜底路由和「未知路径」「文章不存在」都调它。Caddy 这边接法是让这些地址都走 PHP 入口,而不是交给 file_server 发静态文件:
# 旧地址与短链内部重写(必须放在兜底路由之前,否则 not file 会把它们当成未知路径)
@err404 path /404.html /404
rewrite @err404 /404.php
@err403 path /403.html /403
rewrite @err403 /403.php
# 其余未知路径交给前端控制器,由它决定是 404 还是正常页面
@fallback {
not file
path *
}
rewrite @fallback /index.php这么改之后 /404.html 返回的是真正的 404 状态码(以前作为静态文件是 200),页面本身又和全站共用模板。Nginx 想照做也不难:error_page 404 /404.php; 配合 fastcgi_pass 指到 PHP-FPM,或者用 try_files 把未知路径交给前端控制器 —— 思路一样。
切换过程中的实际踩坑
坑 1:PHP 文件被当作静态文件返回
切换后测试 PHP API,发现返回的是源码而不是执行结果。原因是 Caddy 的 php_fastcgi 和 handle 块的路由顺序问题——handle { try_files ... file_server } 块先匹配到了 .php 文件,直接当静态文件返回了。
解决: 把 php_fastcgi 放在 route {} 块的最前面,或者在 route {} 中使用命名匹配器先匹配 .php 文件:
route {
@php { path *.php }
php_fastcgi @php unix//var/run/php/php8.3-fpm.sock
# 其他路由规则...
handle {
try_files {path} {path}.html {path}/
file_server
}
}坑 2:Caddyfile 的语法细节
Caddyfile 看起来简单,但有些坑:
not header修饰符:在命名匹配器中,not必须用块语法not { header Referer ~* "..." },不能用行内not headerrespond指令:不支持行内{ close },必须写在下一行- 正则必须引号包裹:
path_regexp "^/\."而不是 `path_regexp ^/. handle_errors不支持status匹配器:需要用{ expression {http.error.status_code} == 404 }systemctl reload caddy可能失败:我多次遇到 reload 失败的情况,推荐systemctl restart caddy
坑 3:日志权限
Caddy 默认以 caddy 用户运行,不是 root。创建日志目录时如果没用 chown caddy:caddy,Caddy 会因为没权限写日志而启动失败。需要在启动前创建目录并修改属主:
sudo mkdir -p /var/log/caddy
sudo chown caddy:caddy /var/log/caddy坑 4:自签证书的 SNI 问题
Caddy 的 tls internal 会以站点域名为 CN 生成自签证书。用 IP 地址直接访问 HTTPS 时,会报 TLS 告警(SNI 不匹配),必须通过域名访问。这在 CDN 回源场景下不是问题,但本地调试时需要注意:
# 直接 IP 访问失败(SNI 不匹配)
curl -sk https://127.0.0.1/
# 通过域名访问正常
curl -sk --resolve "3z.ink:443:127.0.0.1" https://3z.ink/最终选择建议
结合这次切换的真实体验,给不同场景的建议:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 个人博客/小站点 | Caddy | 配置简单,自动 HTTPS,省心 |
| 多站点反代 | Caddy | 多个 site block 比 Nginx 的 server block 好维护 |
| 高并发业务(万级 QPS) | Nginx | 生态成熟、性能极致、运维工具链完善 |
| CDN 回源服务器 | Caddy | 自签证书一行搞定,配置量少一半 |
| 团队协作项目 | Nginx | 运维团队熟悉、文档丰富、上下游工具多 |
| 需要 Lua/复杂扩展 | Nginx (OpenResty) | 模块生态 Caddy 比不了 |
总结
如果用一句话总结这次切换的感受:
Nginx 像一把瑞士军刀——功能全面、性能极佳、但学习成本高;Caddy 像一把折叠刀——做同样的事,更轻更快更顺手。
万象存笺切换 Caddy 后运行稳定,配置从 130 行缩减到 60 行,维护起来确实舒心很多。如果你也是个人站长、服务器前面有 CDN、主要需求是 PHP-FPM + 静态文件服务,Caddy 是一个值得考虑的选择。
提醒一点:不要为了切换而切换。如果你的 Nginx 跑得好好的、配置也没有维护负担,强行切换只是增加工作量。但如果你像我一样,每次改 Nginx 配置都要翻文档查 location 优先级,那 Caddy 绝对能提升幸福感。