跳转到内容

第 17 章 · Caddy

一句话:自带自动 HTTPS 的 Web 服务器和反向代理,Caddyfile 里写上域名,证书的申请、续期和 HTTP 跳转 HTTPS 都由它自己完成。 用它还是用别的:几个站点、规则简单、想省掉证书运维,用它;已有 Nginx 配置、要精细调缓冲和超时的,留在 Nginx(第 16 章);本机临时开个文件服务或代理,一条 caddy 命令就够。 核对版本:Caddy 2.11(winget 2.11.7,镜像 caddy:2),2026-10

本机练习用随书 compose 的 web 组,Compose 的用法详见第 13 章。在随书的 compose/ 目录下启动,Caddy 在宿主机的 8081 端口,和 8080 的 Nginx 读同一个 html/ 目录:

终端窗口
docker compose --profile web up -d

想在本机直接用 caddy 命令(17.3 的临时服务、17.4 的一行代理),装一个单文件程序:

终端窗口
winget install CaddyServer.Caddy
终端窗口
brew install caddy

Linux 服务器(Debian/Ubuntu)用官方 apt 源,装完自动作为 systemd 服务 caddy 运行,读 /etc/caddy/Caddyfile:

终端窗口
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 chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

查版本,输出以 v2.11 开头:

终端窗口
caddy version
docker compose exec caddy caddy version

反向代理是什么、为什么挡在应用前面,第 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 都是通过它把新配置或停止指令交给正在运行的进程;新配置有错,旧配置继续生效。

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 字节的响应不压

用随书 compose 起 Caddy,看同一个页面

Section titled “用随书 compose 起 Caddy,看同一个页面”

随书 compose/caddy/Caddyfile 只有一个站点块::80 下 root * /srv 加 file_server,compose/html/ 挂在 /srv 上:

  1. 进入目录:cd compose。
  2. 起 web 组,Nginx 和 Caddy 一起起:docker compose --profile web up -d。
  3. 确认 PORTS 列是 127.0.0.1:8081->80/tcp:docker compose ps caddy。
  4. 取首页,和 8080 的 Nginx 返回同一段 HTML:curl http://localhost:8081。
  5. 看响应头,有 Server: Caddy:curl -I http://localhost:8081。
  6. 在 file_server 上面加一行 redir /home / permanent:code caddy/Caddyfile。
  7. 检查配置,-w 让命令在 Caddyfile 所在目录执行:docker compose exec -w /etc/caddy caddy caddy validate。
  8. 重载:docker compose exec -w /etc/caddy caddy caddy reload。
  9. 确认返回 301 和 Location: /:curl -I http://localhost:8081/home。
  10. 如果不是 301(还是 404),是编辑器保存时换了文件、单文件挂载没跟上,重启容器后再试第 9 步:docker compose restart caddy。
  11. 用完停掉,Nginx 一起停:docker compose --profile web down。

一行命令给本机应用开反向代理

Section titled “一行命令给本机应用开反向代理”

本机装了 caddy,想让 3000 端口的应用临时从 8090 访问。8080 留给随书的 Nginx,这里换一个端口:

  1. 在一个终端里起应用,它会列出当前目录的文件:python -m http.server 3000。
  2. 确认应用本身能访问:curl http://localhost:3000/。
  3. 在另一个终端起代理,日志打在这个终端里:caddy reverse-proxy --from :8090 --to :3000。
  4. 通过 Caddy 访问,看到同样的文件列表:curl http://localhost:8090/。
  5. 在应用终端按 Ctrl+C 停掉应用,再访问,Caddy 连不上后端,返回 502:curl -I http://localhost:8090/。
  6. 用完在代理终端按 Ctrl+C 停止,再确认 8090 已不通:curl http://localhost:8090/。

在 17.1 用 apt 装好 Caddy 的 Linux 服务器上,应用跑在本机 3000 端口:

  1. 在自己电脑上确认域名解析到服务器的公网 IP:nslookup example.com。
  2. 服务器开了 ufw 的,放行 80 和 443:sudo ufw allow 80,443/tcp。
  3. 把 /etc/caddy/Caddyfile 改成 17.5 的最小配置,域名换成自己的:sudo nano /etc/caddy/Caddyfile。
  4. 格式化并写回:sudo caddy fmt --overwrite /etc/caddy/Caddyfile。
  5. 检查配置:sudo caddy validate --config /etc/caddy/Caddyfile。
  6. 重载服务:sudo systemctl reload caddy。
  7. 看证书申请日志,出现 certificate obtained successfully 就成了:journalctl -u caddy --no-pager -n 50。
  8. 确认 HTTPS 生效:curl -I https://example.com。
  9. 确认 HTTP 被跳转,返回 308 和 https:// 的 Location:curl -I http://example.com。

证书到期前 Caddy 自己续期,不需要另配定时任务。

位置与附录 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 [email protected] # 全局选项:证书到期提醒和机构联络用的邮箱
}
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 的地址,保留路径
}
  • 日志里反复出现 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 换端口。
  • Caddy 文档:安装、命令行、Caddyfile 和全部指令的入口,查一条指令的语法和默认值。
  • Caddyfile 概念:全局选项块、站点块、地址写法、匹配器和占位符,看懂别人的 Caddyfile 先读这篇。
  • 自动 HTTPS:什么情况下自动申请证书、本地 CA 和 caddy trust、证书存在哪、申请失败怎么排查。
  • reverse_proxy 指令:多个后端的负载均衡、健康检查、默认转发的请求头和超时。