跳到主要内容

Traefik

Traefik 是一个反向代理,它自己获取 Let's Encrypt 证书, 并从文件和 Docker 标签读取配置。当需要区分访问权限时它很方便: 面板只对你的地址开放,而订阅路径 /api/sub 对 整个互联网开放。

配置的静态部分描述入口点、ACME 和提供者,而 动态部分描述路由器、服务和 middleware。file 提供者会即时重新读取 文件,因此路由的修改无需重启容器即可生效。仅当 你倾向于在面板容器上用标签设置路由时,才需要 docker 提供者。

下面是 Traefik 直接位于面板之前并自己 终止 TLS 的方案。

1. 需要准备什么​

  • 域名 panel.example.com,其 A 记录指向宿主机 IP;
  • 开放的 80 和 443 端口(ACME http-01 必须使用 80 端口);
  • 在 xpanel-network 网络中运行的面板 xpanel;
  • 订阅页面 xpanel-subpage(端口 3010),如果你使用它的话。

如果订阅页面与面板分开提供,就需要域名 sub.example.com。 域名取值必须与面板 .env 中指定的相同: PANEL_DOMAIN、FRONT_END_DOMAIN 和 SUB_PUBLIC_DOMAIN。

开始配置前检查 DNS:

dig +short panel.example.com

该命令应返回你的宿主机 IP。如果响应为空,请等待 注册商更新区域 —— ACME 将无法验证域名。

注意

如果域名通过 Cloudflare 代理(橙云),http-01 不会通过。 在首次签发证书期间,请把记录切换到“仅 DNS”模式。

2. 目录与网络​

mkdir -p /opt/xlada/traefik/config /opt/xlada/traefik/letsencrypt /opt/xlada/traefik/logs
cd /opt/xlada/traefik
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json

创建一次网络并把面板接入其中:

docker network create xpanel-network
docker network connect xpanel-network xpanel
docker network connect xpanel-network xpanel-subpage
说明

Traefik 通过名称 xpanel:3000 与面板通信,不使用 127.0.0.1:3000 的端口发布。面板 docker-compose.yml 中的端口映射可以 保留 —— 它对本地诊断有用。

Traefik 与面板运行在同一个 bridge 网络中,因此能通过容器名 看到它。只发布 80 和 443 端口;3000、3010 和 3001 端口仅在宿主机本身上可访问。

如果你以软件包而不是容器方式安装 Traefik,请把静态配置 放到 /etc/traefik/traefik.yml,动态配置放到 /etc/traefik/config/,并 用 systemd 单元启动它:

/etc/systemd/system/traefik.service
[Unit]
Description=Traefik
After=network-online.target
Wants=network-online.target

[Service]
Restart=on-failure
ExecStart=/usr/local/bin/traefik --configFile=/etc/traefik/traefik.yml

[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable --now traefik

3. Traefik 的 docker-compose​

容器监听 80 和 443,以只读方式读取 Docker socket,把 证书保存在 letsencrypt 目录,并把日志写入 logs。请提前创建 logs 目录,否则 Docker 会以 root 身份创建它。

/opt/xlada/traefik/docker-compose.yml
services:
traefik:
image: traefik:v3.5
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- xpanel-network
ports:
- '0.0.0.0:80:80'
- '0.0.0.0:443:443'
environment:
- TZ=UTC
volumes:
# 用于读取容器标签
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./config:/config:ro
- ./letsencrypt:/letsencrypt
- ./logs:/var/log/traefik

networks:
xpanel-network:
name: xpanel-network
external: true

4. 静态配置​

traefik.yml 文件在容器启动时读取一次。如果修改入口点 或 ACME —— 请重启 Traefik。这里描述的是入口点 web 本身及其到 websecure 的重定向、可信代理、提供者和 证书参数。

/opt/xlada/traefik/traefik.yml
api:
dashboard: true
# 只监听容器内的 loopback:dashboard 不对外
insecure: false

entryPoints:
web:
address: ':80'
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ':443'
# 只信任来自这些来源的 X-Forwarded-* 请求头。
# 没有这个列表,客户端可以伪造 X-Forwarded-For 并绕过 ipAllowList。
forwardedHeaders:
trustedIPs:
- '172.16.0.0/12'
- '173.245.48.0/20'
- '103.21.244.0/22'
- '104.16.0.0/13'
- '172.64.0.0/13'

serversTransport:
# 到面板的内部流量走 HTTP,不需要验证证书
insecureSkipVerify: true

providers:
docker:
exposedByDefault: false
network: xpanel-network
file:
directory: /config
watch: true

certificatesResolvers:
letsencrypt:
acme:
email: you@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web

log:
level: INFO
filePath: /var/log/traefik/traefik.log

accessLog:
filePath: /var/log/traefik/access.log

5. 动态配置​

该文件描述面板和订阅页面的路由器。这里还设置了防护 请求头、IP 限制以及上游服务地址。Host 规则必须 与为其签发证书的域名完全一致。

/opt/xlada/traefik/config/xlada.yml
http:
routers:
# 订阅页面 —— 公开流量,无限制
xlada-sub:
rule: 'Host(`sub.example.com`)'
entryPoints:
- websecure
service: subpage
tls:
certResolver: letsencrypt

# 构建产物静态资源:按文件名中的哈希设置长缓存
xlada-assets:
rule: 'Host(`panel.example.com`) && PathPrefix(`/assets/`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
- xlada-static-cache
service: panel
tls:
certResolver: letsencrypt
priority: 100

# 面板:仅你的 IP,外加防护请求头
xlada-panel:
rule: 'Host(`panel.example.com`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
- xlada-ip-allow
service: panel
tls:
certResolver: letsencrypt

middlewares:
xlada-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: strict-origin-when-cross-origin
# 流量到达 websecure,因此 Traefik 会自行设置
# X-Forwarded-Proto: https —— 面板会接受这样的请求

xlada-static-cache:
headers:
customResponseHeaders:
Cache-Control: 'public, max-age=31536000, immutable'

xlada-ip-allow:
ipAllowList:
sourceRange:
- '203.0.113.10/32'
- '198.51.100.0/24'
ipStrategy:
# depth = 1 表示:在 X-Forwarded-For 链中取距可信代理
# 一个位置的地址,并把它视为客户端地址
depth: 1

services:
panel:
loadBalancer:
servers:
- url: 'http://xpanel:3000'
subpage:
loadBalancer:
servers:
- url: 'http://xpanel-subpage:3010'

6. 启动​

cd /opt/xlada/traefik
docker compose up -d
docker compose logs -f

证书会出现在 letsencrypt/acme.json 中。该文件必须保持 不可公开访问:chmod 600。如果日志中出现 ACME 错误,请检查端口 80 是否能从外部访问,以及 DNS 记录是否尚未通过 CDN 代理。

7. 公开的订阅路径​

在上面的配置中,面板整体被 IP 列表关闭,而订阅页面 在单独的域名上开放。如果未部署订阅页面,那么应当保持公开的 只有面板域名上的 /api/sub/ 路径:该地址会写入 profile-web-page-url 请求头,客户端通过它下载订阅。

为此,在同一个域名上声明两个路由器:一个用于 /api/sub/,不带 限制;另一个用于其余所有内容 —— 带 IP 列表。Traefik 会选择 匹配前缀更长的路由器,因此声明顺序并不重要,而规则应只在 该路径上相交。

/opt/xlada/traefik/config/xlada.yml
http:
routers:
# 单独域名上的订阅页面 —— 公开流量
xlada-sub:
rule: 'Host(`sub.example.com`)'
entryPoints:
- websecure
service: subpage
tls:
certResolver: letsencrypt

# 面板域名上公开的订阅路径
xlada-sub-path:
rule: 'Host(`panel.example.com`) && PathPrefix(`/api/sub/`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
service: panel
tls:
certResolver: letsencrypt
priority: 110

# 构建产物静态资源:按文件名中的哈希设置长缓存
xlada-assets:
rule: 'Host(`panel.example.com`) && PathPrefix(`/assets/`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
- xlada-static-cache
service: panel
tls:
certResolver: letsencrypt
priority: 100

# 面板:仅你的 IP,外加防护请求头
xlada-panel:
rule: 'Host(`panel.example.com`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
- xlada-ip-allow
service: panel
tls:
certResolver: letsencrypt

middlewares:
xlada-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: strict-origin-when-cross-origin

xlada-static-cache:
headers:
customResponseHeaders:
Cache-Control: 'public, max-age=31536000, immutable'

xlada-ip-allow:
ipAllowList:
sourceRange:
- '203.0.113.10/32'
- '198.51.100.0/24'
ipStrategy:
depth: 1

services:
panel:
loadBalancer:
servers:
- url: 'http://xpanel:3000'
subpage:
loadBalancer:
servers:
- url: 'http://xpanel-subpage:3010'

修改后请检查两种场景:对 /api/sub/AbCdEf1234567890 的请求 应返回不带 403 的响应,而面板根路径从他人地址访问 应仍不可用。 请记住,面板 .env 中 SUB_PUBLIC_DOMAIN 的值必须与 所选方案一致:要么是 sub.example.com,要么是 panel.example.com/api/sub。

8. 面板容器的标签​

同样的路由也可以用标签而不是文件来描述。在这种方案中, xlada.yml 里只保留 middleware,而 Traefik 从容器标签读取 路由和服务。下面是两个文件的完整内容。

/opt/xlada/traefik/config/xlada.yml
http:
middlewares:
xlada-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: strict-origin-when-cross-origin

xlada-static-cache:
headers:
customResponseHeaders:
Cache-Control: 'public, max-age=31536000, immutable'

xlada-ip-allow:
ipAllowList:
sourceRange:
- '203.0.113.10/32'
- '198.51.100.0/24'
ipStrategy:
depth: 1
/opt/xlada/docker-compose.yml
services:
xpanel-db:
image: postgres:18.4
container_name: xpanel-db
restart: unless-stopped
env_file: .env
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
- TZ=UTC
volumes:
- xpanel-db-data:/var/lib/postgresql
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
networks:
- xpanel-network

xpanel-redis:
image: valkey/valkey:9-alpine
container_name: xpanel-redis
restart: unless-stopped
command: >
valkey-server --save "" --appendonly no
--maxmemory-policy noeviction --loglevel warning
healthcheck:
test: ['CMD', 'valkey-cli', 'ping']
interval: 5s
timeout: 3s
retries: 10
networks:
- xpanel-network

xpanel:
image: ghcr.io/xray-panel/backend:latest
container_name: xpanel
restart: unless-stopped
env_file: .env
environment:
- REDIS_HOST=xpanel-redis
- REDIS_PORT=6379
- REDIS_DB=0
depends_on:
xpanel-db:
condition: service_healthy
xpanel-redis:
condition: service_healthy
networks:
- xpanel-network
labels:
- 'traefik.enable=true'
- 'traefik.docker.network=xpanel-network'

# 公开的订阅路径
- 'traefik.http.routers.xlada-sub.rule=Host(`panel.example.com`) && PathPrefix(`/api/sub/`)'
- 'traefik.http.routers.xlada-sub.entrypoints=websecure'
- 'traefik.http.routers.xlada-sub.tls.certresolver=letsencrypt'
- 'traefik.http.routers.xlada-sub.middlewares=xlada-headers@file'
- 'traefik.http.routers.xlada-sub.service=xlada'
- 'traefik.http.routers.xlada-sub.priority=110'

# 带长缓存的构建产物静态资源
- 'traefik.http.routers.xlada-assets.rule=Host(`panel.example.com`) && PathPrefix(`/assets/`)'
- 'traefik.http.routers.xlada-assets.entrypoints=websecure'
- 'traefik.http.routers.xlada-assets.tls.certresolver=letsencrypt'
- 'traefik.http.routers.xlada-assets.middlewares=xlada-headers@file,xlada-static-cache@file'
- 'traefik.http.routers.xlada-assets.service=xlada'
- 'traefik.http.routers.xlada-assets.priority=100'

# 面板:仅你的 IP
- 'traefik.http.routers.xlada-panel.rule=Host(`panel.example.com`)'
- 'traefik.http.routers.xlada-panel.entrypoints=websecure'
- 'traefik.http.routers.xlada-panel.tls.certresolver=letsencrypt'
- 'traefik.http.routers.xlada-panel.middlewares=xlada-headers@file,xlada-ip-allow@file'
- 'traefik.http.routers.xlada-panel.service=xlada'

- 'traefik.http.services.xlada.loadbalancer.server.port=3000'

xpanel-subpage:
image: ghcr.io/xray-panel/subscription-page:latest
container_name: xpanel-subpage
restart: unless-stopped
env_file: .env.subpage
depends_on:
- xpanel
networks:
- xpanel-network
labels:
- 'traefik.enable=true'
- 'traefik.docker.network=xpanel-network'
- 'traefik.http.routers.xlada-subdomain.rule=Host(`sub.example.com`)'
- 'traefik.http.routers.xlada-subdomain.entrypoints=websecure'
- 'traefik.http.routers.xlada-subdomain.tls.certresolver=letsencrypt'
- 'traefik.http.routers.xlada-subdomain.middlewares=xlada-headers@file'
- 'traefik.http.routers.xlada-subdomain.service=xlada-subpage'
- 'traefik.http.services.xlada-subpage.loadbalancer.server.port=3010'

networks:
xpanel-network:
name: xpanel-network
external: true

volumes:
xpanel-db-data:
name: xpanel-db-data

不要同时为同一个路由器使用文件和标签 —— 这会造成 定义冲突。在这种方案中,路由和服务只由标签描述, 而 xlada.yml 文件只包含 middleware。

9. 客户端真实 IP​

正确的 IP 是 HWID 限制、审计和登录防暴力破解所必需的。 Traefik 会填入 X-Forwarded-For,但只信任来自 forwardedHeaders.trustedIPs 中地址的传入请求头。

用一个具体例子来拆解这条链。客户端 A 经过代理 B 到达 Traefik T:

X-Forwarded-For: A, B
  • depth: 1 —— 取距可信代理一个位置的地址(在示例中 是 A),也就是真实客户端;
  • excludedIPs —— 会预先从链中丢弃列出的地址; 当 Traefik 前面有 Cloudflare 时使用这种方式:在列表中 列出 Cloudflare 网段,Traefik 就能看到客户端,而不是 CDN 地址。

如果面板看到的是同一个地址,请检查:

  • Docker 网络网段和 Cloudflare 地址已加入 trustedIPs;
  • ipStrategy 中指定了正确的 depth 或 excludedIPs 列表;
  • 请求头没有被中间代理重写。

如果 Traefik 前面有 Cloudflare,请额外按 Cloudflare Tunnel 页面所述配置对 CF-Connecting-IP 的信任。

10. 检查与诊断​

# 检查证书和重定向
curl -sI http://panel.example.com | head -3
curl -sI https://panel.example.com | head -3

# 公开的订阅路径应能在不受 IP 限制的情况下响应
curl -s -o /dev/null -w '%{http_code}\n' https://sub.example.com/

# 查看 Traefik 看到的 IP
tail -n 20 /opt/xlada/traefik/logs/access.log

docker compose logs traefik | tail -50

Traefik dashboard 已启用,但没有对外发布。可以这样访问它。 socat 守护进程会把容器的 loopback 端口转发到宿主机,之后 就可以使用普通的 SSH 隧道:

# 把 traefik 容器的 8080 端口显示在宿主机 localhost 上
socat TCP-LISTEN:8080,fork,reuseaddr \
TCP:$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' traefik):8080

# 在你自己的机器上
ssh -L 8080:127.0.0.1:8080 user@panel.example.com

然后在浏览器中打开 http://127.0.0.1:8080/dashboard/。如果 需要长期使用 dashboard,更安全的做法是用带 ipAllowList 和 basicAuth 的单独路由器关闭它,而不是无保护地发布。

提示

除了硬性的 IP 列表,也可以通过 Cloudflare Zero Trust 或 VPN 关闭面板。只有当管理员拥有静态 IP 时,地址列表才可靠。

接下来​