跳到主要内容

Caddy

概览​

Caddy 是一个反向代理,它自己获取并续期 Let's Encrypt 证书:无需单独的 ACME 客户端、cron 和手动重载。XLADA 面板 和订阅页面只能在带 HTTPS 的代理后面工作 —— 如果请求到达时没有 X-Forwarded-For 和 X-Forwarded-Proto: https 请求头,它们的 middleware 会断开连接。

Caddy 会自行设置 X-Forwarded-*。在下面的配置中我们仍然 显式地设置它们:这样即使 Caddy 前面出现 CDN 或负载均衡器,行为也不会改变,X-Forwarded-Proto 也不会变成 http。

示例中的域名是 panel.example.com 和 sub.example.com,上游是 127.0.0.1:3000(面板)和 127.0.0.1:3010(订阅页面)—— 正是 xpanel 和 xpanel-subpage 容器在宿主机 loopback 上发布的端口。

注意

只有拥有已注册的域名才能继续:没有域名 Let's Encrypt 不会 签发证书。

DNS​

检查服务器 IP 地址(最好是静态的),并在 DNS 服务商或注册商处创建记录:

类型名称值
Apanel服务器的 IPv4 地址
Asub服务器的 IPv4 地址
AAAApanel服务器的 IPv6 地址
AAAAsub服务器的 IPv6 地址

在 Cloudflare 中,记录添加在 DNS → Records → Create record 部分:类型 A,名称字段填 panel 或 sub,地址字段填服务器 IP。

  • 灰云 —— 流量直接到达你的服务器,Caddy 能看到客户端真实 IP。 如果 Cloudflare 不可用或造成干扰,就选择这种模式。
  • 橙云 —— 流量经过 Cloudflare 网络:服务器 IP 被隐藏, 并启用攻击防护。此时必须在 Caddyfile 的 trusted_proxies static 中指定 Cloudflare 网段,否则所有客户端都会看起来像 同一个 Cloudflare 地址,HWID 限制、审计和登录防暴力破解 都会失去意义。

等待记录更新:dig +short panel.example.com 应返回 服务器地址,橙云情况下则返回 Cloudflare 地址。

证书​

只要在 Caddyfile 中指定了域名,Caddy 就会自动获取证书。 验证域名需要开放 80(HTTP-01)和 443(TLS-ALPN-01)端口; 续期也是自动的 —— 大约在到期前 30 天进行。在 全局块中只需指定 email:该地址会收到证书相关问题的 通知。

完整 Caddyfile​

创建目录和文件:

sudo mkdir -p /opt/caddy && cd /opt/caddy
sudo nano Caddyfile
/opt/caddy/Caddyfile
# ─── 全局参数 ────────────────────────────────────────────────────────
{
# Let's Encrypt 通知邮箱
email admin@example.com

# Cloudflare 后面的客户端真实 IP。网段由 Cloudflare 发布;
# 应定期把该列表与最新版本核对。
servers {
trusted_proxies static \
173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 \
141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 \
197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 \
104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 \
2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 \
2a06:98c0::/29 2c0f:f248::/32
client_ip_headers CF-Connecting-IP X-Forwarded-For
}
}

# ─── XLADA 面板 ──────────────────────────────────────────────────────
panel.example.com {
encode zstd gzip

# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}

# 构建产物静态资源:文件名中的哈希允许长期缓存
@assets path /assets/*
header @assets Cache-Control "public, max-age=31536000, immutable"

reverse_proxy 127.0.0.1:3000 {
# 没有这两个请求头,面板会断开连接。
# 重写 X-Forwarded-For,避免客户端伪造自己的 IP。
header_up X-Forwarded-For {client_ip}
header_up X-Forwarded-Proto https
header_up X-Real-IP {client_ip}
header_up Host {host}

# WebSocket:Caddy 自行处理 Upgrade 和 Connection,
# 无需单独的请求头。flush_interval -1 会关闭
# 缓冲 —— 这对 SSE 和统计流式传输很重要。
flush_interval -1
}
}

# ─── 订阅页面 ────────────────────────────────────────────────────────
sub.example.com {
encode zstd gzip

header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}

# 构建产物静态资源:文件名中的哈希允许长期缓存
@assets path /assets/*
header @assets Cache-Control "public, max-age=31536000, immutable"

reverse_proxy 127.0.0.1:3010 {
header_up X-Forwarded-For {client_ip}
header_up X-Forwarded-Proto https
header_up X-Real-IP {client_ip}
header_up Host {host}
flush_interval -1
}
}

# ─── 按 IP 和未知域名的请求 ──────────────────────────────────────────
:443 {
tls internal
respond 204
}

关键指令:

指令用途
trusted_proxies static信任来自这些子网的 Cloudflare 请求头
client_ip_headers从 CF-Connecting-IP 获取真实 IP
header_up X-Forwarded-For把真正的客户端 IP 传给面板
header_up X-Forwarded-Proto https面板只接受 https
flush_interval -1无缓冲地流式返回
@assets 和 header为 /assets/ 中的构建产物静态资源设置长缓存
:443 搭配 tls internal拦截按 IP 扫描的扫描器
注意

不要为 sub.example.com 域名添加额外的 HTTP 认证: 订阅页面会被客户端应用程序请求,它们无法输入 用户名和密码。面板登录请用其自身的认证和 TOTP 保护。

在 Docker 中运行​

/opt/caddy/docker-compose.yml
services:
caddy:
image: caddy:2.9
container_name: caddy
restart: unless-stopped
# 需要 host 模式,Caddy 才能看到 127.0.0.1:3000 和 127.0.0.1:3010
network_mode: host
environment:
- TZ=UTC
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./data:/data # 证书和 ACME 密钥
- ./config:/config # Caddy 的内部状态
cd /opt/caddy
docker compose up -d
docker compose logs -f -t

选择 host 模式是有意为之:只有这样才能让 Caddy 容器看到面板和 订阅页面的 loopback 端口,同时证书保留在宿主机上。80 和 443 端口由 Caddy 自己监听,而 3000、3010 和 3001 不对外开放。

检查与重载​

# 不重启检查语法
caddy validate --config /opt/caddy/Caddyfile

# 将文件格式化为规范形式
caddy fmt --overwrite /opt/caddy/Caddyfile

# 平滑重载:连接不会中断
systemctl reload caddy

# 在 Docker 中,同样的命令在容器内执行
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

日志中该看什么​

journalctl -u caddy -f # 安装在宿主机
docker compose logs -f caddy # 在 Docker 中运行
  • certificate obtained successfully —— 证书已签发;
  • serving initial configuration —— 配置已被接受;
  • tls.obtain 或 acme —— 请检查 DNS、80 和 443 端口、Let's Encrypt 限额;
  • dial tcp 127.0.0.1:3000: connect: connection refused —— 面板未启动。

确认两个站点都有响应:

curl -I https://panel.example.com
curl -I https://sub.example.com

响应应以 HTTP/2 200 开头。客户端真实 IP 可以在 面板审计中方便地检查:如果那里是 Cloudflare 地址,说明 trusted_proxies 没有启用。

接下来​