文章目录
镜像版本:
caddy:2.11.4(h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0=)。低于 2.7 的版本没有trusted_proxies全局选项,配置写法会不一样。
简介
Caddy 是一个基于 Go 编写的 HTTP/2、HTTP/3 web 服务器,反向代理、静态托管、TLS 自动化开箱即用。相比 Nginx,Caddy 提供静态二进制文件,零依赖启动,在国产化服务器上不用处理 glibc、pcre、openssl、zlib 的版本适配问题。
功能:正向代理,反向代理,静态托管,IP 封禁。
正向代理:客户端代理,如 VPN。代理客户端。
反向代理:服务器端的代理,代理后端服务。
IP 封禁:基于真实客户端 IP 做黑白名单拦截。
下载与安装
Caddy 官网下载地址:https://caddy.com/download
GitHub Release 地址:https://github.com/caddyserver/caddy/releases
shell
# 启动 Caddy
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
# 后台运行
caddy start --config /etc/caddy/Caddyfile --adapter caddyfile
# 停止 Caddy
caddy stop
# 重新加载配置(admin 未关闭时)
caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
# 验证配置
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
# 格式化 Caddyfile
caddy fmt --overwrite /etc/caddy/Caddyfile
# 查看版本
caddy version
Caddyfile 配置文件结构
Caddyfile 采用就近配置的语法。外层大括号是全局选项,site block 是站点配置。
caddyfile
{
# 全局选项,影响所有站点
auto_https off
admin off
servers {
trusted_proxies static xx.xx.xx.xx/32
client_ip_headers X-Forwarded-For X-Real-IP
}
}
:8528 {
# 站点配置
bind 0.0.0.0
route {
handle /api/* {
reverse_proxy 127.0.0.1:8899
}
}
}
CIDR 掩码与 trusted_proxies
trusted_proxies 后面跟的是 IP 范围,写法是 CIDR 记法。
xx.xx.xx.xx/32 → 只有 xx.xx.xx.xx 这一个 IP
xx.xx.xx.0/24 → xx.xx.xx.0 ~ xx.xx.xx.255,整段 256 个 IP
xx.xx.0.0/16 → xx.xx.0.0 ~ xx.xx.255.255,整段 65536 个 IP
xx.xx.xx.xx → 不写尾数,Caddy 当作 /32
/数字 是前缀长度,表示子网掩码中 1 的个数:
/32= 255.255.255.255,只匹配 1 个 IP/24= 255.255.255.0,匹配 256 个 IP/16= 255.255.0.0,匹配 65536 个 IP/0= 0.0.0.0,匹配所有 IP
caddyfile
servers {
# 信任直连 Caddy 的上一层代理出口 IP
trusted_proxies static xx.xx.xx.xx/32
# 信任整段出口网段
# trusted_proxies static xx.xx.xx.0/24
# 信任所有私网段(生产慎用)
# trusted_proxies static private_ranges
# 解析真实 IP 的请求头,按顺序取第一个非空
client_ip_headers X-Forwarded-For X-Real-IP
}
private_ranges 等于 192.168.0.0/16、172.16.0.0/12、10.0.0.0/8、127.0.0.1/8、fd00::/8、::1。
trusted_proxies 的判定逻辑:
- 源 IP 在列表内 → 从
client_ip_headers取client_ip - 源 IP 不在 →
client_ip = remote_ip(直连对端 IP)
IP 封禁失效最常见的根因就是 trusted_proxies 配错:来源不在白名单,Caddy 不使用 XFF,client_ip 永远是代理 IP,黑名单命中不到。
静态资源代理
静态页面配置
caddyfile
handle_path /m/* {
root * /var/www/site-m
try_files {path} {path}/ /index.html
file_server
}
说明:
handle_path自动剥离/m前缀root *指定文件系统根目录try_files {path} {path}/ /index.html兜底,找不到文件时回退到index.html,前端路由不 404file_server处理 mime 推断、range 请求
handle 与 handle_path
| 指令 | 前缀处理 | 用途 |
|---|---|---|
handle /prefix/* |
不剥 | 反向代理、保留前缀的静态 |
handle_path /prefix/* |
剥前缀 | 静态 SPA,upstream 期望根路径 |
保留前缀的静态资源:
caddyfile
handle /fe/* {
root * /var/www/site-fe
try_files {path} {path}/ /index.html
file_server
}
附件下载
caddyfile
handle_path /files/* {
root * /var/www/files
file_server
header Content-Disposition attachment
header Content-Type application/octet-stream
}
Content-Disposition attachment 让浏览器作为下载处理,Content-Type application/octet-stream 作为兜底 mime,避免文本解析。
反向代理
Http 反向代理
由于使用反向代理,后端服务无法获取用户的真实 IP 地址,所以需要设置 header 信息。
caddyfile
handle /api/* {
uri replace /api /upstream-path
reverse_proxy 127.0.0.1:8899 {
header_up X-Real-IP {http.request.header.X-Real-IP}
header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
}
}
说明:
uri replace /api /upstream-path把/api替换为/upstream-pathreverse_proxy转发到后端,默认追加X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Hostheader_up透传客户端原头给后端
常用的 uri 处理指令
| 指令 | 作用 |
|---|---|
uri strip_prefix /prefix |
剥前缀 |
uri strip_suffix .html |
剥后缀 |
uri replace /old /new |
替换路径片段 |
uri replace /old /new 5 |
限制替换次数 |
保留前缀的反向代理
caddyfile
handle /upstream/* {
reverse_proxy 127.0.0.1:8527
}
/upstream/users/1 透传给 127.0.0.1:8527/upstream/users/1。
header_up 注意事项
不要把 {client_ip} 占位符塞进 header_up。原因:
reverse_proxy自己按trusted_proxies清洗 XFF 链- 上游(Spring、Express、Go)用各自的
trust proxy配置读真实 IP - 手工塞占位符反而污染代理链
健康检查与负载均衡
caddyfile
reverse_proxy 127.0.0.1:8899 127.0.0.1:8898 {
lb_policy round_robin
lb_retries 2
health_uri /health
health_interval 10s
}
动静分离
- 为什么需要动静分离?
Tomcat 主要是用来处理 servlet 请求。处理 css、js、图片这些静态文件的 IO 性能不够好,将静态文件交给 Caddy 处理,可以提高系统的访问速度,减少后端请求次数,给后端服务器降压。
- Caddy 配置文件示例:
caddyfile
:8528 {
handle /api/* {
reverse_proxy 127.0.0.1:8080
}
handle /static/* {
root * /var/www/static
file_server
}
handle / {
root * /var/www/site
try_files {path} {path}/ /index.html
file_server
}
}
- 匹配优先级
Caddy 同一 route 内从上到下匹配,更具体的路径放前面,通用兜底放后面。
caddyfile
route {
# 具体路径先匹配
handle /files/zhlt_file/* {
root * /var/www/zhlt_file
file_server
}
# 通用兜底
handle /files/* {
root * /var/www/files
file_server
}
}
IP 封禁
封禁文件 blocked_ips.caddyfile
一行一条,CIDR 也直接写:
caddyfile
client_ip xx.xx.xx.xx
client_ip xx.xx.xx.xx/24
单行多地址:
caddyfile
client_ip xx.xx.xx.xx xx.xx.xx.xx xx.xx.xx.xx/24
Caddy 把同一 @blocked 命名匹配器内的多行 client_ip 合并为 OR,命中任一返回 403。
配置文件挂载
caddyfile
route {
@blocked {
import blocked_ips.caddyfile
}
respond @blocked "Forbidden" 403
# 业务 handle 写在后面
}
client_ip 匹配器 vs 直接比 XFF 头
错误写法(永远不命中):
caddyfile
@blocked expression `{http.request.header.X-Forwarded-For} == "xx.xx.xx.xx"`
XFF 一般是 client, proxy1, proxy2 的逗号链,整串等值比较不可能命中。
正确写法:
caddyfile
@blocked client_ip xx.xx.xx.xx
respond @blocked "Forbidden" 403
client_ip 匹配器对 Caddy 解析出的单值 IP 做等值或 CIDR 比较。
route 强制前置
Caddy 对指令有内置排序。respond 写在 route 外面、且放在 handle 后面,会被某个业务 handle 提前接管。
caddyfile
route {
@blocked { import blocked_ips.caddyfile }
respond @blocked "Forbidden" 403
# 业务 handle 写在后面
}
route 把整段指令按书写顺序锁定执行。封禁检查放最前,业务路径就不会漏过。
白名单
只放行内网:
caddyfile
@denied not client_ip private_ranges
abort @denied
管理脚本 run_caddy.sh
Caddy 命令不好记,写了一个管理脚本,封装 start/stop/restart/reload/check/status 六个动作,并自带配置校验。脚本放在 Caddy 二进制同级目录。
bash
#!/bin/bash
# Caddy 管理脚本
BASE_DIR="/home/soft/caddy"
CADDY_BIN="$BASE_DIR/caddy"
CADDY_CONFIG="$BASE_DIR/Caddyfile"
CADDY_PIDFILE="$BASE_DIR/caddy.pid"
CADDY_LOGFILE="$BASE_DIR/caddy.log"
# 确保日志和 PID 目录存在
mkdir -p "$(dirname "$CADDY_PIDFILE")" "$(dirname "$CADDY_LOGFILE")"
# 校验配置
check_config() {
if ! $CADDY_BIN validate --config "$CADDY_CONFIG" >/dev/null 2>&1; then
echo "ERROR: Caddy 配置文件语法错误!"
$CADDY_BIN validate --config "$CADDY_CONFIG"
exit 1
else
echo "INFO: Caddy 配置文件语法正确。"
fi
}
# 获取当前 PID
get_pid() {
if [ -f "$CADDY_PIDFILE" ]; then
cat "$CADDY_PIDFILE"
fi
}
# 检查是否运行
is_running() {
local pid=$(get_pid)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
return 0
else
return 1
fi
}
case "$1" in
start)
if is_running; then
echo "WARNING: Caddy 已在运行 (PID: $(get_pid))。"
exit 0
fi
check_config
echo "INFO: 启动 Caddy..."
nohup $CADDY_BIN run --config "$CADDY_CONFIG" --adapter caddyfile > "$CADDY_LOGFILE" 2>&1 &
echo $! > "$CADDY_PIDFILE"
echo "SUCCESS: Caddy 已启动 (PID: $!)"
;;
stop)
if ! is_running; then
echo "WARNING: Caddy 未运行。"
exit 0
fi
echo "INFO: 停止 Caddy..."
kill $(get_pid)
rm -f "$CADDY_PIDFILE"
echo "SUCCESS: Caddy 已停止。"
;;
restart)
echo "INFO: 重启 Caddy..."
$0 stop
sleep 2
$0 start
;;
reload)
if ! is_running; then
echo "ERROR: Caddy 未运行,无法重载。"
exit 1
fi
echo "INFO: 热重载 Caddy 配置..."
check_config
kill -USR1 $(get_pid)
echo "SUCCESS: 配置已重载。"
;;
check)
check_config
;;
status)
if is_running; then
echo "INFO: Caddy 正在运行 (PID: $(get_pid))"
else
echo "INFO: Caddy 未运行"
fi
;;
*)
echo "用法: $0 {start|stop|restart|reload|check|status}"
exit 1
;;
esac
使用方式:
bash
# 启动
./run_caddy.sh start
# 停止
./run_caddy.sh stop
# 重启
./run_caddy.sh restart
# 热重载(admin off 时通过 USR1 信号)
./run_caddy.sh reload
# 校验配置语法
./run_caddy.sh check
# 查看运行状态
./run_caddy.sh status
更新黑名单后:
bash
./run_caddy.sh check && ./run_caddy.sh reload
脚本说明:
BASE_DIR为 Caddy 二进制所在目录,按实际部署路径修改start用nohup后台运行,PID 写入caddy.pid,日志输出到caddy.logreload通过kill -USR1发信号,Caddy 收到 USR1 会重新加载配置check在 start/reload 前自动调用,配置语法错误直接退出,不会启动失败进程
日志
caddyfile
log {
output file logs/access.log {
roll_size 100mb
roll_keep 10
roll_keep_for 720h
}
format json
}
roll_size单文件超过 100MB 触发切割roll_keep最多保留 10 个历史文件roll_keep_for 720h30 天后强制清理format json方便 ELK / Loki 收
控制台日志全局配置:
caddyfile
log {
output stdout
level DEBUG
}
access.log 字段说明
json
{
"request": {
"remote_ip": "yy.yy.yy.yy",
"client_ip": "xx.xx.xx.xx",
"method": "GET",
"uri": "/"
}
}
remote_ip 是直连 Caddy 的对端,client_ip 是经 trusted_proxies 解出的真实 IP。封禁判定的对象始终是 client_ip。
完整 Caddyfile
caddyfile
{
auto_https off
admin off
servers {
trusted_proxies static xx.xx.xx.xx/32
client_ip_headers X-Forwarded-For X-Real-IP
listener_wrappers {
proxy_protocol
}
}
log {
output stdout
level DEBUG
}
}
:8528 {
bind 0.0.0.0
route {
@blocked {
import blocked_ips.caddyfile
}
respond @blocked "Forbidden" 403
handle /api/v2/* {
reverse_proxy 127.0.0.1:9680
}
handle /api/* {
uri replace /api /upstream-path
reverse_proxy 127.0.0.1:8899 {
header_up X-Real-IP {http.request.header.X-Real-IP}
header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
}
}
handle /upstream/* {
reverse_proxy 127.0.0.1:8527
}
handle_path /m/* {
root * /var/www/site-m
try_files {path} {path}/ /index.html
file_server
}
handle_path /s/* {
root * /var/www/site-s
try_files {path} {path}/ /index.html
file_server
}
handle /fe/* {
root * /var/www/site-fe
try_files {path} {path}/ /index.html
file_server
}
handle_path /files/* {
root * /var/www/files
file_server
header Content-Disposition attachment
header Content-Type application/octet-stream
}
}
log {
output file logs/access.log {
roll_size 100mb
roll_keep 10
roll_keep_for 720h
}
format json
}
}
验证
bash
# 验证配置
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
# docker 验证,不污染本机
docker run --rm -v "$(pwd):/etc/caddy:ro" caddy:2.11.4 \
caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
# 模拟请求,看封禁是否生效
curl -v -H "X-Forwarded-For: xx.xx.xx.xx" http://127.0.0.1:8528/
Empty reply from server → 命中 abort / 403,配置没漏。
返回 200 → 配置没生效,查 trusted_proxies / client_ip_headers / client_ip 匹配器。
常见问题
| 现象 | 原因 | 修法 |
|---|---|---|
| 真实 IP 永远是代理 IP | trusted_proxies 没配或填错 |
把直连 Caddy 的代理出口 IP 加进去 |
X-Real-IP == xx.xx.xx.xx 不命中 |
直接比原始头,XFF 是逗号链 | 改用 client_ip 匹配器 |
/ 能拦,/api/* 漏过 |
respond 在 route 外被 handle 接管 |
把封禁包进 route 钉在最前 |
duplicate listener 启动失败 |
servers {} 在多个 import 文件里重复定义 |
导入文件不要再写 servers |
| 客户端 TLS / 握手失败 | listener_wrappers { proxy_protocol } 开了但上游没发 PROXY 头 |
跟上游对齐,或临时关 wrapper |
注意事项
trusted_proxies范围要小,不要塞0.0.0.0/0- 黑名单走
client_ip匹配器,不要直接比 XFF 头 - 封禁与业务 handle 包进同一个
route,封禁写最前 admin off之后,更新配置只能systemctl restart caddy- 不要将 root 目录配置成
/或/root - 真实 IP、内网域名、路径前缀不进文档、备份、仓库
bash
# 落盘前 sed 替换真实 IP 为占位符
sed -i -E 's/([0-9]{1,3}\.){3}[0-9]{1,3}/xx.xx.xx.xx/g' blocked_ips.caddyfile