Страница подписки на том же сервере, что панель
Панель — 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
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
из конфига не удаляйте. Другие прокси — в разделе
Обратный прокси.
Настройка в панели
-
Впишите токен в
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 -
Проверьте ссылку: 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