订阅页面与面板部署在同一台服务器
面板 — xray-panel/backend · 节点 — xray-panel/node · 订阅页面 — xray-panel/subscription-page
订阅页面向用户展示其连接的美化列表:客户端链接、二维码、有效期和剩余流量。
它通过 API 令牌从面板获取数据,不会向浏览器暴露面板的内部结构。客户端在域名
sub.example.com 上通过路径中的短代码打开它。
这种方案适用于面板和订阅页面位于同一台主机的情况。 容器通过 Docker 内部网络互通,只有 nginx 对外提供服务。 如果面板和订阅需要部署在不同机器上,请转到 在单独服务器上部署订阅页面。
何时需要订阅页面
如果你想要独立的子域名 sub.example.com、自定义设计以及独立于面板的更新,
就需要自己的订阅页面。面板可以提供一个内置页面,但它的外观不同,并且位于面板
的域名上。只要 SUB_PUBLIC_DOMAIN 中填的是
panel.example.com/api/sub 这类值,客户端拿到的就是内置页面,
而不是你安装的页面。
需要什么
| 项目 | 值 |
|---|---|
| 域名 | sub.example.com |
| 面板 | 在同一台主机上,容器 xpanel |
| API 令牌 | 在面板中创建 |
| Docker 和 Docker Compose | 已安装 |
| nginx 和 certbot | 已安装 |
| 本地端口 | 3010 |
API 令牌在面板中创建:Settings → API Tokens → Create token。该值 只显示一次 —— 请保存它。
面板和订阅页面都会在请求缺少 X-Forwarded-For 和
X-Forwarded-Proto: https 头时断开连接。因此页面前面必须有带 TLS 的反向代理。
环境变量文件
创建目录和环境变量文件:
mkdir -p /opt/xlada-subpage && cd /opt/xlada-subpage
cat > .env <<'EOF'
APP_PORT=3010
# 面板位于同一个 Docker 网络中 —— 通过容器名访问。
REMNAWAVE_PANEL_URL=http://xpanel:3000
REMNAWAVE_API_TOKEN=在此粘贴面板令牌
# 页面不在根路径时使用的路径前缀。两端不要带斜杠。
CUSTOM_SUB_PREFIX=
# 确定客户端 IP 时信任多少层反向代理。
TRUST_PROXY=1
# 用于外部鉴权的 X-Api-Key:带 security 插件的 Caddy 或 Tiny Auth。
CADDY_AUTH_API_TOKEN=
# Cloudflare Zero Trust。
CLOUDFLARE_ZERO_TRUST_CLIENT_ID=""
CLOUDFLARE_ZERO_TRUST_CLIENT_SECRET=""
# 兼容旧的 Marzban 链接。
MARZBAN_LEGACY_LINK_ENABLED=false
MARZBAN_LEGACY_SECRET_KEY=
MARZBAN_LEGACY_SUBSCRIPTION_VALID_FROM=
EOF
chmod 600 .env
REMNAWAVE_* 这些名称是为兼容性保留的 —— 这样该页面也能与旧版 Remnawave
面板配合。空变量请保持原样。
Docker Compose
services:
xpanel-subpage:
image: ghcr.io/xray-panel/subscription-page:latest
container_name: xpanel-subpage
restart: unless-stopped
env_file: .env
ports:
- 127.0.0.1:3010:3010
networks:
- xpanel-network
networks:
xpanel-network:
name: xpanel-network
external: true
网络 xpanel-network 已由面板创建,因此这里以外部网络方式接入。启动容器:
docker compose up -d
docker compose logs -f xpanel-subpage
页面监听 127.0.0.1:3010,不对外暴露:只有 nginx 能访问它。
证书
先创建一个临时的 80 端口 server 块,用于响应 certbot 校验:
sudo mkdir -p /var/www/certbot
sudo nano /etc/nginx/sites-available/xlada-subpage
# /etc/nginx/sites-available/xlada-subpage
# 用于签发证书的临时 server 块。
server {
listen 80;
listen [::]:80;
server_name sub.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}
sudo ln -sf /etc/nginx/sites-available/xlada-subpage /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt-get update && sudo apt-get install -y certbot
sudo certbot certonly --webroot -w /var/www/certbot -d sub.example.com
完整 nginx 配置
将文件 /etc/nginx/sites-available/xlada-subpage 替换为完整配置:
# /etc/nginx/sites-available/xlada-subpage
server {
listen 80;
listen [::]:80;
server_name sub.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name sub.example.com;
ssl_certificate /etc/letsencrypt/live/sub.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sub.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 1d;
ssl_session_cache shared:XLADA_TLS:10m;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
proxy_pass http://127.0.0.1:3010;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}
sudo nginx -t && sudo systemctl reload nginx
sudo certbot renew --dry-run
续期时同样需要验证域名所有权,因此不要删除配置中的 /var/www/certbot 路径。
其他代理见反向代理。
面板中的设置
-
将令牌填入
/opt/xlada-subpage/.env的REMNAWAVE_API_TOKEN,然后 重启容器:cd /opt/xlada-subpage && docker compose up -d -
在面板的
/opt/xlada/.env中指定订阅的公开地址:SUB_PUBLIC_DOMAIN=sub.example.com该值不带
https://,也不带结尾斜杠。如果设置了CUSTOM_SUB_PREFIX=sub,请填写SUB_PUBLIC_DOMAIN=sub.example.com/sub。 -
重启面板,让它重新读取文件:
cd /opt/xladadocker compose up -d xpanel -
检查链接:Users → 选择用户 → Subscription link。 地址应以
https://sub.example.com/开头:面板会用SUB_PUBLIC_DOMAIN和用户的短标识符拼出它。
TRUST_PROXY 和 CUSTOM_SUB_PREFIX
TRUST_PROXY=1 表示页面前面正好有一层反向代理。如果层数更多,该值也要修改:
取值表见单独服务器页面。
CUSTOM_SUB_PREFIX 用于指定路径,当页面不在域名根路径时使用:
CUSTOM_SUB_PREFIX=sub
SUB_PUBLIC_DOMAIN=sub.example.com/sub
两个值都不带斜杠,且必须一致。更多内容见 单独服务器页面。
检查与常见错误
# 容器已启动并监听本地端口
docker compose ps
docker compose logs --tail 50 xpanel-subpage
# 页面有响应:预期为 HTTP/2 200
curl -sI https://sub.example.com/AbCdEf1234567890 | head -n 5
成功响应包含 HTTP/2 200 和 profile-web-page-url 头。
| 现象 | 原因与处理 |
|---|---|
| 响应为空,连接中断 | 缺少 X-Forwarded-For 或 X-Forwarded-Proto: https;检查 nginx 中的 location 块 |
502 Bad Gateway | 容器未启动,或 nginx 没有把请求转发到 127.0.0.1:3010 |
401 或 403 | REMNAWAVE_API_TOKEN 错误或已被撤销;请创建新令牌 |
| 页面超时失败 | 容器内无法访问面板:xpanel 名称拼写错误,或未接入 xpanel-network 网络 |
| 日志中客户端地址不正确 | TRUST_PROXY 设置错误;请填写页面前的代理层数 |
订阅链接返回 404 | CUSTOM_SUB_PREFIX 与 SUB_PUBLIC_DOMAIN 不一致 |
容器和 nginx 日志:
docker compose logs -f xpanel-subpage
sudo tail -f /var/log/nginx/error.log