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

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

Исходный код

Панель — 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​

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. Впишите токен в 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
# Контейнер запущен и слушает локальный порт
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, клиенты будут получать встроенную страницу панели, а не установленную вами.

Что дальше​