跳到主要内容

在单独服务器上部署订阅页面

源代码

面板 — xray-panel/backend · 节点 — xray-panel/node · 订阅页面 — xray-panel/subscription-page

订阅页面向用户展示其连接的美化列表:客户端链接、二维码、有效期和剩余流量。 它通过 API 令牌从面板获取数据。客户端在域名 sub.example.com 上通过路径中的 短代码打开它。

何时需要单独服务器​

当订阅页面需要与面板隔离时会选择单独服务器:负载分散到多台机器上,面板主机上 也不会多出额外服务。页面通过域名 https://panel.example.com 访问面板 API, 因此面板必须能从互联网经 HTTPS 访问。如果面板和订阅位于同一台主机,请使用 订阅页面与面板部署在同一台服务器页面。

需要什么​

项目值
域名sub.example.com
面板可从外部通过 https://panel.example.com 访问
API 令牌在面板中创建
Docker 和 Docker Compose在新服务器上
nginx 和 certbot在新服务器上
本地端口3010

令牌在面板中创建:Settings → API Tokens → Create token。该值只显示一次 —— 请保存它。它与面板部署在同一台服务器时使用的是同一个令牌。

准备服务器​

sudo apt-get update
sudo apt-get install -y docker.io docker-compose-v2 nginx certbot
sudo systemctl enable --now docker
docker compose version

如果没有 docker-compose-v2 软件包,请按官方说明安装 Docker Engine, 并检查 docker compose version。

环境变量文件​

mkdir -p /opt/xlada-subpage && cd /opt/xlada-subpage

cat > .env <<'EOF'
APP_PORT=3010

# 面板可从外部通过 HTTPS 访问。
REMNAWAVE_PANEL_URL=https://panel.example.com
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

Docker Compose​

docker-compose.yml
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
driver: bridge

这台服务器上没有面板,因此 xpanel-network 网络在本地创建。 页面监听 127.0.0.1:3010,不对外暴露:只有 nginx 能访问它。启动容器:

docker compose up -d
docker compose logs -f xpanel-subpage

证书​

新服务器上的 80 端口是空闲的,因此可以在配置 nginx 之前签发证书:

sudo certbot certonly --standalone -d sub.example.com

完整 nginx 配置​

# /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 ln -sf /etc/nginx/sites-available/xlada-subpage /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot renew --dry-run

TRUST_PROXY​

TRUST_PROXY 是 Express 的 trust proxy 设置。真实客户端地址来自 X-Forwarded-For 头,而它很容易被伪造,因此需要告诉应用信任哪些代理层。

值何时设置
1页面前面正好有一层代理 —— 常见情况
2两层,例如先 Cloudflare,再 nginx
true信任所有;仅用于隔离网络中的调试
false不信任任何人,无法确定真实 IP
127.0.0.1, 172.16.0.0/12信任列出的地址和子网

只有确实存在额外代理层时才填写它们:过度信任会让客户端在请求头中 伪造他人的地址。

CUSTOM_SUB_PREFIX​

默认情况下页面在域名根路径提供。如果需要路径,请设置不带首尾斜杠的 CUSTOM_SUB_PREFIX,并在面板中填写相同的路径:

CUSTOM_SUB_PREFIX=sub
SUB_PUBLIC_DOMAIN=sub.example.com/sub

两个值必须一致,否则客户端会拿到指向不存在路径的链接。不能把页面代理到任意 location:路径由这个变量决定,而 nginx 仍然把根路径转发到 127.0.0.1:3010。

面板中的设置与检查​

  1. 将令牌填入 /opt/xlada-subpage/.env 的 REMNAWAVE_API_TOKEN,然后 重启容器:

    cd /opt/xlada-subpage && docker compose up -d
  2. 在面板服务器的 /opt/xlada/.env 中指定订阅的公开地址:

    SUB_PUBLIC_DOMAIN=sub.example.com

    该值不带 https://,也不带结尾斜杠。如果设置了 CUSTOM_SUB_PREFIX=sub,请填写 SUB_PUBLIC_DOMAIN=sub.example.com/sub。

  3. 重启面板,让它重新读取文件:

    cd /opt/xlada
    docker compose up -d xpanel
# 容器已启动并监听本地端口
docker compose ps
docker compose logs --tail 50 xpanel-subpage

# 页面有响应:预期为 HTTP/2 200
curl -sI https://sub.example.com/AbCdEf1234567890 | head -n 5

# 面板必须能从外部响应
curl -sI https://panel.example.com | head -n 1

成功响应包含 HTTP/2 200 和 profile-web-page-url 头。在面板中这样检查 链接:Users → 选择用户 → Subscription link —— 地址应以 https://sub.example.com/ 开头。

注意

只要 SUB_PUBLIC_DOMAIN 中填的是 panel.example.com/api/sub 这类值, 客户端拿到的就是面板内置页面,而不是你安装的页面。

接下来​