Marzban-node - сервер-нода панели Marzban. Нужен для упрощения процесса настройки множества серверов в рамках одной VPN подписки. Поставляется с ядром XRay-core.
Здесь рассмотрим установку ноды с использованием механизма self-steal.
Установка
Перед установкой ноды необходимо установить Docker. Инструкции по установке Docker Engine с плагином Docker Compose есть на их официальном сайте. Также, необходимо иметь уже настроенную основную панель Marzban.
Установка ноды на сервер осуществляется скриптом, предоставленным разработчиком панели:
sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban-node.sh)" @ installПосле загрузки всех необходимых пакетов скрипт запросит сертификат ноды. Получить его можно в интерфейсе основной панели: открываем меню в правом верхнем углу → Node settings → Add New Marzban Node → нажимаем на иконку глаза и тем самым раскрываем окно с сертификатом ноды. Копируем и передаем скрипту. Окно с добавлением ноды при этом не закрываем.

После утверждения сертификата в скрипте подтверждаем порты для XRay ядра и API (или меняем, если это требуется). Скрипт установит docker compose с нодой. Заполняем данные в форме на основной панели, нажимаем Add Node, ждем подключения основной панели к ноде.
Tip
Если на раскрывающемся элементе ноды в списке отображается зеленая плашка Connected и версия XRay, значит установка прошла успешно.
Дополнительная настройка
После успешного добавления ноды к основной панели приступим к настройке ноды. Для начала переходим на сервере переходим в директорию /opt/marzban-node и выключаем компоуз командой docker compose down. Далее нам нужно будет отредактировать docker-compose.yml.
services:
caddy: # добавляем в компоуз контейнер caddy
image: caddy:2.9
restart: always
network_mode: host
volumes:
- ./caddy/data:/data
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./caddy/index.html:/www/index.html
marzban-node:
container_name: marzban-node
image: gozargah/marzban-node:latest
restart: always
network_mode: host
environment:
SSL_CLIENT_CERT_FILE: "/var/lib/marzban-node/cert.pem"
SERVICE_PORT: "62050"
XRAY_API_PORT: "62051"
SERVICE_PROTOCOL: "rest"
volumes:
- /var/lib/marzban-node:/var/lib/marzban
- /var/lib/marzban-node:/var/lib/marzban-nodeСоздаем директорию для конфигурации Caddy командой mkdir caddy и создаем в ней два файла:
-
Caddyfile{ https_port 4123 default_bind 127.0.0.1 servers { listener_wrappers { proxy_protocol { allow 127.0.0.1/32 } tls } } auto_https disable_redirects } https://node.domain { # заменить домен на тот, который вы планируете использовать для ноды root * /www try_files {path} {path}.html {path}/ =404 file_server encode gzip } http://node.domain { # заменить домен на тот, который вы планируете использовать для ноды bind 0.0.0.0 redir https://node.domain{uri} permanent # заменить домен на тот, который вы планируете использовать для ноды } :4123 { tls internal respond 204 } :80 { bind 0.0.0.0 respond 204 } -
index.html<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Вход в Confluence</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background-color: #f4f5f7; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } .login-container { background-color: white; padding: 40px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); width: 350px; text-align: center; } .logo { margin-bottom: 20px; } .logo img { width: 120px; } h2 { margin-bottom: 20px; font-size: 24px; color: #0052cc; } input[type="text"], input[type="password"] { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #dfe1e6; border-radius: 4px; box-sizing: border-box; font-size: 16px; } .error { border-color: red; } .error-message { color: red; font-size: 14px; display: none; margin-top: 10px; } button { width: 100%; padding: 10px; background-color: #0052cc; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin-top: 20px; } button:hover { background-color: #0747a6; } .help-links { margin-top: 20px; font-size: 14px; } .help-links a { color: #0052cc; text-decoration: none; } .help-links a:hover { text-decoration: underline; } /* Стили для модального окна */ .modal { display: none; position: fixed; z-index: 1; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.4); padding-top: 60px; } .modal-content { background-color: white; margin: 5% auto; padding: 20px; border: 1px solid #888; width: 80%; max-width: 400px; border-radius: 8px; text-align: center; } .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; } .close:hover, .close:focus { color: black; text-decoration: none; cursor: pointer; } </style> </head> <body> <div class="login-container"> <div class="logo"> <img src="https://cdn.icon-icons.com/icons2/2429/PNG/512/confluence_logo_icon_147305.png" alt="Confluence" /> </div> <h2 id="login-title">Войти в Confluence</h2> <form id="login-form"> <input type="text" id="username" name="username" placeholder="Адрес электронной почты" /> <input type="password" id="password" name="password" placeholder="Введите пароль" /> <button type="submit" id="login-button">Войти</button> </form> <div id="error-message" class="error-message"> Неправильное имя пользователя или пароль. </div> <div class="help-links"> <a href="#" id="forgot-link">Не удается войти?</a> • <a href="#" id="create-link">Создать аккаунт</a> </div> </div> <div id="myModal" class="modal"> <div class="modal-content"> <span class="close">×</span> <p id="modal-text">Для создания аккаунта обратитесь к администратору.</p> </div> </div> <script> function setLanguage(lang) { const elements = { ru: { loginTitle: "Войти в Confluence", usernamePlaceholder: "Адрес электронной почты", passwordPlaceholder: "Введите пароль", loginButton: "Войти", forgotLink: "Не удается войти?", createLink: "Создать аккаунт", createAccountText: "Для создания аккаунта обратитесь к администратору.", forgotPasswordText: "Для восстановления доступа обратитесь к администратору.", errorMessage: "Неправильное имя пользователя или пароль.", }, en: { loginTitle: "Login to Confluence", usernamePlaceholder: "Email address", passwordPlaceholder: "Enter password", loginButton: "Login", forgotLink: "Can't log in?", createLink: "Create an account", createAccountText: "To create an account, please contact your administrator.", forgotPasswordText: "To recover access, please contact your administrator.", errorMessage: "Incorrect username or password.", }, } document.getElementById("login-title").innerText = elements[lang].loginTitle document.getElementById("username").placeholder = elements[lang].usernamePlaceholder document.getElementById("password").placeholder = elements[lang].passwordPlaceholder document.getElementById("login-button").innerText = elements[lang].loginButton document.getElementById("forgot-link").innerText = elements[lang].forgotLink document.getElementById("create-link").innerText = elements[lang].createLink // Устанавливаем тексты для модальных окон и сообщений об ошибках document.getElementById("create-link").dataset.modalText = elements[lang].createAccountText document.getElementById("forgot-link").dataset.modalText = elements[lang].forgotPasswordText document.getElementById("error-message").innerText = elements[lang].errorMessage } function detectLanguage() { const userLang = navigator.language || navigator.userLanguage if (userLang.startsWith("ru")) { setLanguage("ru") } else { setLanguage("en") } } document.addEventListener("DOMContentLoaded", detectLanguage) var modal = document.getElementById("myModal") var span = document.getElementsByClassName("close")[0] function openModal(text) { document.getElementById("modal-text").innerText = text modal.style.display = "block" } document.getElementById("create-link").onclick = function (event) { event.preventDefault() openModal(this.dataset.modalText) } document.getElementById("forgot-link").onclick = function (event) { event.preventDefault() openModal(this.dataset.modalText) } span.onclick = function () { modal.style.display = "none" } window.onclick = function (event) { if (event.target == modal) { modal.style.display = "none" } } document.getElementById("login-form").onsubmit = function (event) { event.preventDefault() var username = document.getElementById("username") var password = document.getElementById("password") var errorMessage = document.getElementById("error-message") username.classList.remove("error") password.classList.remove("error") errorMessage.style.display = "none" var hasError = false if (username.value.trim() === "") { username.classList.add("error") hasError = true } if (password.value.trim() === "") { password.classList.add("error") hasError = true } if (hasError) { return } errorMessage.style.display = "block" } window.onclick = function (event) { if (event.target == modal) { modal.style.display = "none" } } </script> </body> </html>
Далее поднимаем компоуз командой docker compose up -d.
В основной панели открываем конфигурацию xray и добавляем в поле serverName домен ноды

Warning
В случае, если у вас используется несколько нод, их домены должны быть записаны в массив serverNames вместе с доменом основной панели.
Наконец, переходим в настройки хостов: открываем меню в правом верхнем углу → Host settings
Раскрываем панель с нужным инбаундом (скорее всего он у вас только один) и добавляем нового хоста, заполняя поля настроек следующим образом:

Success
При успешной настройке по этой инструкции на домене, использованном для ноды, будет открываться темплейт логина в Confluence, который, само собой, ничего не делает. Данный ход нужен для обеспечения легитности трафика и защиты сервера.