Страница подписки на отдельном сервере
Панель — xray-panel/backend · нода — xray-panel/node · страница подписки — xray-panel/subscription-page
Страница подписки отдаёт пользователю оформленный список его подключений: ссылки
для клиентов, QR-коды, срок действия и остаток трафика. Данные она берёт из панели
по API-токену. Клиент открывает её на домене sub.example.com с коротким кодом
в пути.
Когда нужен отдельный сервер
Отдельный сервер берут, когда страница подписки должна быть изолирована от
панели: нагрузка распределяется между машинами, а на хосте панели не появляется
лишних сервисов. Страница обращается к API панели по домену
https://panel.example.com, поэтому панель обязана быть доступна из интернета по
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 для внешней авторизации: Caddy с security-аддоном или 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
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.
Настройка в панели и проверка
-
Впишите токен в
REMNAWAVE_API_TOKENв/opt/xlada-subpage/.envи перезапустите контейнер: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
# Контейнер запущен и слушает локальный порт
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,
клиенты будут получать встроенную страницу панели, а не установленную вами.