Перейти к основному содержимому

Страница подписки на том же сервере, что панель

Исходный код

Панель — xray-panel/backend · нода — xray-panel/node · страница подписки — xray-panel/subscription-page

Страница подписки отдаёт пользователю оформленный список его подключений: ссылки для клиентов, QR-коды, срок действия и остаток трафика. Данные она берёт из панели по 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 для внешней авторизации: 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

Имена REMNAWAVE_* сохранены для совместимости — так страница работает и со старой панелью Remnawave. Пустые переменные оставьте как есть.

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
external: true

Сеть xpanel-network уже создана панелью, поэтому здесь она подключается как внешняя. Запустите контейнер:

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

Страница слушает 127.0.0.1:3010 и наружу не смотрит: доступ к ней даёт только nginx.

Сертификат​

Сначала создайте временный блок на 80 порту, который отдаёт проверку certbot:

sudo mkdir -p /var/www/certbot
sudo nano /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;
}
}
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 из конфига не удаляйте. Другие прокси — в разделе Обратный прокси.

Настройка в панели​

  1. Впишите токен в REMNAWAVE_API_TOKEN в /opt/xlada-subpage/.env и перезапустите контейнер:

    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
  4. Проверьте ссылку: 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; проверьте блок location в nginx
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

Что дальше​