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 单元启动它:
[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 身份创建它。
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 的重定向、可信代理、提供者和
证书参数。
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 规则必须
与为其签发证书的域名完全一致。
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 会选择
匹配前缀更长的路由器,因此声明顺序并不重要,而规则应只在
该路径上相交。
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 从容器标签读取
路由和服务。下面是两个文件的完整内容。
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
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 时,地址列表才可靠。