第 17 章 · Caddy
一句话:自带自动 HTTPS 的 Web 服务器和反向代理,Caddyfile 里写上域名,证书的申请、续期和 HTTP 跳转 HTTPS 都由它自己完成。 用它还是用别的:几个站点、规则简单、想省掉证书运维,用它;已有 Nginx 配置、要精细调缓冲和超时的,留在 Nginx(第 16 章);本机临时开个文件服务或代理,一条
caddy命令就够。 核对版本:Caddy 2.11(winget 2.11.7,镜像caddy:2),2026-10
17.1 安装与版本
Section titled “17.1 安装与版本”本机练习用随书 compose 的 web 组,Compose 的用法详见第 13 章。在随书的 compose/ 目录下启动,Caddy 在宿主机的 8081 端口,和 8080 的 Nginx 读同一个 html/ 目录:
docker compose --profile web up -d想在本机直接用 caddy 命令(17.3 的临时服务、17.4 的一行代理),装一个单文件程序:
winget install CaddyServer.Caddybrew install caddyLinux 服务器(Debian/Ubuntu)用官方 apt 源,装完自动作为 systemd 服务 caddy 运行,读 /etc/caddy/Caddyfile:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curlcurl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpgcurl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.listsudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg /etc/apt/sources.list.d/caddy-stable.listsudo apt updatesudo apt install caddy查版本,输出以 v2.11 开头:
caddy versiondocker compose exec caddy caddy version17.2 五分钟概念
Section titled “17.2 五分钟概念”反向代理是什么、为什么挡在应用前面,第 16 章 16.2 已经讲过,这里只讲 Caddy 自己的东西。
- Caddyfile:Caddy 的配置文件。最上面可以有一个不带名字的
{ }全局选项块,其余每个“地址 + 大括号”是一个站点块(site block),块里一行一条指令。只有一个站点时大括号可以省掉。 - 站点地址即配置:站点块开头的地址决定监听什么、要不要 HTTPS。写
example.com就自动申请证书、监听 443 并把 80 跳到 443;写localhost用 Caddy 自建的本地证书;写:80或http://开头则只走 HTTP。随书的 Caddyfile 写的是:80,所以容器里不碰证书。 - 指令(directive)与匹配器(matcher):
file_server、reverse_proxy、redir、encode都是指令;指令后面紧跟的/api/*是路径匹配器,只让这类请求走这条指令。同一块里的指令由 Caddy 按固定顺序执行,与书写顺序无关,redir总在file_server之前。/api/*不包括/api本身。 - 自动 HTTPS(automatic HTTPS):对公网域名,Caddy 向 Let’s Encrypt 等机构申请证书,到期前自己续期,证书存在数据目录里。前提是域名已解析到这台机器、80 和 443 对公网开放。
- 管理 API(admin API):Caddy 运行时在
localhost:2019开一个只对本机的接口。caddy reload和caddy stop都是通过它把新配置或停止指令交给正在运行的进程;新配置有错,旧配置继续生效。
17.3 常用命令
Section titled “17.3 常用命令”caddy run、reload、validate 不带 --config 时读当前目录的 Caddyfile;caddy fmt 没有 --config,文件路径直接跟在后面。随书 compose 里在 reload、validate 前加 docker compose exec -w /etc/caddy caddy;容器里的 Caddyfile 是只读挂载,格式化在宿主机上用本机 caddy 做:caddy fmt --overwrite caddy/Caddyfile。Caddyfile 写法一组写在站点块里。
| 任务 | 命令 | 说明 |
|---|---|---|
| 运行与重载 | ||
| 前台运行 | caddy run |
日志直接打在终端,Ctrl+C 停止 |
| 后台启动 | caddy start |
启动后命令返回,进程留在后台 |
| 停止 | caddy stop |
通过 2019 管理 API 让进程优雅退出 |
| 重载配置 | caddy reload |
不断开现有连接;新配置有错时报错并保留旧配置 |
| 格式化 | caddy fmt --overwrite |
统一缩进并写回文件;不加 --overwrite 只打印结果 |
| 检查配置 | caddy validate |
解析并加载一遍配置,不真正启动,出错给出文件名和行号 |
| 看生效的配置 | curl localhost:2019/config/ |
返回 Caddyfile 转换后的 JSON |
| 一条命令的临时服务 | ||
| 共享当前目录 | caddy file-server --browse --listen :8000 |
浏览器打开 http://localhost:8000 看到目录列表 |
| 临时反向代理 | caddy reverse-proxy --from :8090 --to :3000 |
8090 的请求转给 3000,不用写 Caddyfile |
| 信任本地证书 | caddy trust |
Caddy 运行时执行,经 2019 管理 API 取本地 CA 根证书装进系统信任库,会要求管理员权限 |
| Caddyfile 写法 | ||
| 静态站点 | root * /var/www/site 换行 file_server |
root 指目录,file_server 负责读文件,两行都要写 |
| 反向代理 | reverse_proxy localhost:3000 |
Host 原样转给后端,自动加 X-Forwarded-For、X-Forwarded-Proto,WebSocket 不用另配 |
| 按路径代理 | reverse_proxy /api/* localhost:3000 |
路径原样转发;要去掉 /api 前缀改用 handle_path /api/* { … } |
| 跳转 | redir https://example.com{uri} permanent |
{uri} 带上原路径和参数;permanent 是 301,不写是 302 |
| 压缩 | encode |
默认开启 zstd 和 gzip;小于 512 字节的响应不压 |
17.4 典型工作流
Section titled “17.4 典型工作流”用随书 compose 起 Caddy,看同一个页面
Section titled “用随书 compose 起 Caddy,看同一个页面”随书 compose/caddy/Caddyfile 只有一个站点块::80 下 root * /srv 加 file_server,compose/html/ 挂在 /srv 上:
- 进入目录:
cd compose。 - 起 web 组,Nginx 和 Caddy 一起起:
docker compose --profile web up -d。 - 确认 PORTS 列是
127.0.0.1:8081->80/tcp:docker compose ps caddy。 - 取首页,和 8080 的 Nginx 返回同一段 HTML:
curl http://localhost:8081。 - 看响应头,有
Server: Caddy:curl -I http://localhost:8081。 - 在
file_server上面加一行redir /home / permanent:code caddy/Caddyfile。 - 检查配置,
-w让命令在 Caddyfile 所在目录执行:docker compose exec -w /etc/caddy caddy caddy validate。 - 重载:
docker compose exec -w /etc/caddy caddy caddy reload。 - 确认返回
301和Location: /:curl -I http://localhost:8081/home。 - 如果不是 301(还是 404),是编辑器保存时换了文件、单文件挂载没跟上,重启容器后再试第 9 步:
docker compose restart caddy。 - 用完停掉,Nginx 一起停:
docker compose --profile web down。
一行命令给本机应用开反向代理
Section titled “一行命令给本机应用开反向代理”本机装了 caddy,想让 3000 端口的应用临时从 8090 访问。8080 留给随书的 Nginx,这里换一个端口:
- 在一个终端里起应用,它会列出当前目录的文件:
python -m http.server 3000。 - 确认应用本身能访问:
curl http://localhost:3000/。 - 在另一个终端起代理,日志打在这个终端里:
caddy reverse-proxy --from :8090 --to :3000。 - 通过 Caddy 访问,看到同样的文件列表:
curl http://localhost:8090/。 - 在应用终端按
Ctrl+C停掉应用,再访问,Caddy 连不上后端,返回502:curl -I http://localhost:8090/。 - 用完在代理终端按
Ctrl+C停止,再确认 8090 已不通:curl http://localhost:8090/。
服务器上给域名自动 HTTPS
Section titled “服务器上给域名自动 HTTPS”在 17.1 用 apt 装好 Caddy 的 Linux 服务器上,应用跑在本机 3000 端口:
- 在自己电脑上确认域名解析到服务器的公网 IP:
nslookup example.com。 - 服务器开了 ufw 的,放行 80 和 443:
sudo ufw allow 80,443/tcp。 - 把
/etc/caddy/Caddyfile改成 17.5 的最小配置,域名换成自己的:sudo nano /etc/caddy/Caddyfile。 - 格式化并写回:
sudo caddy fmt --overwrite /etc/caddy/Caddyfile。 - 检查配置:
sudo caddy validate --config /etc/caddy/Caddyfile。 - 重载服务:
sudo systemctl reload caddy。 - 看证书申请日志,出现
certificate obtained successfully就成了:journalctl -u caddy --no-pager -n 50。 - 确认 HTTPS 生效:
curl -I https://example.com。 - 确认 HTTP 被跳转,返回
308和https://的Location:curl -I http://example.com。
证书到期前 Caddy 自己续期,不需要另配定时任务。
17.5 配置要点
Section titled “17.5 配置要点”位置与附录 C 一致:
| 配置项 | Linux(apt) | Windows | Docker 镜像内 |
|---|---|---|---|
| Caddyfile | /etc/caddy/Caddyfile |
运行 caddy 时当前目录的 Caddyfile |
/etc/caddy/Caddyfile(随书挂载的就是它) |
| 数据目录(证书、本地 CA) | /var/lib/caddy/.local/share/caddy |
%AppData%\Caddy |
/data |
| 配置目录(自动保存的配置) | /var/lib/caddy/.config/caddy |
%AppData%\Caddy |
/config |
| 日志 | journalctl -u caddy |
运行它的终端 | docker compose logs caddy |
数据目录里是证书和私钥,不是缓存,删了会重新申请证书,频繁重申可能撞上 Let’s Encrypt 的频率限制。Docker 跑在服务器上时要给 /data 挂命名卷;随书 compose 只走 :80,没有挂。
Linux 服务器上一个最小的站点,保存为 /etc/caddy/Caddyfile:
{ email admin@example.com # 全局选项:证书到期提醒和机构联络用的邮箱}
example.com { # 写域名就自动申请证书、监听 443、80 跳 443 encode # 开启 zstd 和 gzip 压缩 reverse_proxy localhost:3000 # 全部请求转给本机 3000 端口的应用}
www.example.com { # 带 www 的域名也会申请自己的证书 redir https://example.com{uri} permanent # 301 跳到不带 www 的地址,保留路径}17.6 注意事项
Section titled “17.6 注意事项”- 日志里反复出现
could not get certificate或challenge failed,HTTPS 打不开。 原因:域名没解析到这台机器,或 80、443 被防火墙、云安全组挡住,证书机构验证不了。处理:nslookup核对 IP,放开两个端口后sudo systemctl reload caddy。 - 本机访问
https://localhost时浏览器提示证书不受信(NET::ERR_CERT_AUTHORITY_INVALID)。 原因:证书由 Caddy 自建的本地 CA 签发,系统不认识这个 CA。处理:Caddy 运行时执行一次caddy trust;随书 compose 走:80,没有本地 CA,不涉及。 caddy validate报Unexpected '}' because no matching opening brace, at Caddyfile:5或Unexpected '{' on a new line。 原因:大括号没配对,或{另起了一行。处理:caddy fmt Caddyfile看缩进出的结构,改好再validate。caddy reload后页面没变,或报no config file to load。 原因:不在 Caddyfile 所在目录执行,或 Docker 里单文件挂载在编辑器保存后没同步进容器。处理:加--config <路径>再 reload;随书 compose 用docker compose restart caddy。- 启动失败,日志有
listen tcp :80: bind: address already in use。 原因:同一台机器上的 Nginx 或其他程序占着 80 端口。处理:sudo ss -ltnp | grep :80找出进程,停掉 Nginx(sudo systemctl disable --now nginx)或给 Caddy 换端口。
17.7 深入阅读
Section titled “17.7 深入阅读”- Caddy 文档:安装、命令行、Caddyfile 和全部指令的入口,查一条指令的语法和默认值。
- Caddyfile 概念:全局选项块、站点块、地址写法、匹配器和占位符,看懂别人的 Caddyfile 先读这篇。
- 自动 HTTPS:什么情况下自动申请证书、本地 CA 和 caddy trust、证书存在哪、申请失败怎么排查。
- reverse_proxy 指令:多个后端的负载均衡、健康检查、默认转发的请求头和超时。