MMHXYZ 技术笔记

个人站从备案到 HTTPS:Caddy + Let's Encrypt 的三个坑

2026-10-30 · 运维 · 约 5 分钟

给一个个人项目配域名和证书,本来以为是一小时的事。实际花的时间多得多,因为每个故障的表现都在误导我。

坑一:页面打不开,问题不在页面

最开始的症状是"打开是一片空白/超时"。我第一反应是去查页面内容、查服务有没有起。

真正的原因:只有 api 这个子域配了 A 记录,主域 @ 和 www 根本没解析。浏览器拿到 NXDOMAIN,连服务器都没连上。

dig 一条命令就能排除:

dig +short example.com
dig +short www.example.com
dig +short api.example.com

前两条空、第三条有 IP —— 答案就在这儿。

教训:任何"打不开"的问题,先确认 DNS,再怀疑一切。

顺带一个细节:域名服务商的"关联已有网站"这类功能,有时候会顺手帮你写一条根域的 A 记录,但不一定写 www。所以两个都要单独确认,不要假定它们一起生效。

坑二:一个域名解析不了,整个站点块的证书都签不出来

Caddy 的配置里,同一个站点块可以写多个域名:

example.com, www.example.com {
    root * /var/www
    file_server
}

这看起来很自然。但如果 www 那一刻还没解析,Let's Encrypt 的 HTTP-01 校验会拿到 NXDOMAIN,整个块的证书申请直接失败——包括本来完全正常的 example.com。

失败之后更麻烦:Caddy 会进入指数退避,短时间内不再尝试。这时候 systemctl reload caddy 是没用的,配置重载了,但退避状态还在。必须:

sudo systemctl restart caddy

这个区别我卡了很久:reload 看起来"生效了",日志里却一直不签发。

修法是在部署脚本里按实际解析情况决定包含哪些域名,而不是写死:

if getent hosts www.example.com >/dev/null 2>&1; then
  HOSTS="example.com, www.example.com"
else
  HOSTS="example.com"
  echo "www 未解析,本次只绑主域"
fi

宁可少绑一个域名,也不要把整个块的签发拖死。

坑三:路径多一级,全站 404

Caddy 的 root 写错一级,所有静态资源都 404,但服务本身、证书、反向代理全部正常。

我的是:

handle_path /updates/* {
    root * /opt/app/deploy/updates      # 少了 deploy/ 这一级 → 全站 404
    file_server
}

handle_path 会剥掉 /updates 前缀,所以 root 要给到"剥掉之后仍能拼出正确路径"的那一层。写的时候心里要清楚:剥掉前缀之后,剩下的是 /manifest.json,拼在哪。

改配置前先备份,改完先校验再 reload:

sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak-$(date +%Y%m%d-%H%M%S)
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl reload caddy

validate 通过不代表路由对,但至少能挡住语法错误把整个站点搞挂。

反向代理:白名单比全放更省事

一开始我把 /api/* 整个反代到后端,省事。后来改成显式白名单:

@proxy path /api/health /api/register /api/login /api/logout /api/profile
handle @proxy {
    request_body {
        max_size 32KB
    }
    reverse_proxy 127.0.0.1:8766
}

@api path /api/*
handle @api {
    respond "Not Found" 404
}

好处有三个:

  • 后端以后新增了没打算公开的接口,不会被顺带暴露
  • 管理类接口不需要额外的鉴权就天然不可达
  • max_size 顺手挡住了超大请求

白名单的代价是每加一个接口要改两处(Caddy 和代码)。这个成本我接受,因为它强制我在"要不要公开"这件事上做一次明确决定。

多入口共用一份站点

三个域名(主域、www、api 子域)指向同一个目录、同一套路由,用 Caddy 的 snippet 复用:

(site) {
    # ... 所有路由规则
}

api.example.com {
    import site
}

example.com, www.example.com {
    import site
}

这样维护的永远只有一份配置。之前我担心"三个域名要维护三份",实际完全不用——域名和站点是两个层次的东西。

小结

三个坑的共同点:症状和原因隔着一层。页面打不开是因为 DNS,证书签不出是因为另一个域名,404 是因为路径层级。

所以现在我的排查顺序固定成:DNS → 证书 → 配置语法 → 路由 → 应用。逐层往下,不在最上面那层猜。

← 返回全部笔记