Блог Clearway

Marzban: установка и настройка панели для VPN-оператора

Коротко

Установить Marzban — одна команда на чистой Ubuntu 22.04/24.04 или Debian 12 с Docker: sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban.sh)" @ install. Скрипт кладёт .env и docker-compose.yml в /opt/marzban/, данные и xray_config.json — в /var/lib/marzban/. Дальше настройка Marzban в три шага: marzban cli admin create --sudo, затем UVICORN_HOST = "127.0.0.1" + nginx с TLS вместо открытого :8000, затем первый VLESS Reality-инбаунд правкой xray_config.json. Ноды подключаются образом gozargah/marzban-node по портам 62050/62051 клиентским сертификатом из панели. Реалистичный срок — 30–40 минут на панель с одной нодой. Главная оговорка: апстрим Gozargah/Marzban давно без крупных релизов, свежий Xray-core и XHTTP приходится ставить руками через core-update — отдельно на панели и отдельно на каждой ноде.

Что такое Marzban и кому он подходит

Marzban — веб-панель поверх Xray-core: пользователи, лимиты трафика, сроки, подписки, мультинода. Написана на Python (FastAPI + SQLAlchemy), ядро дёргается через Xray gRPC API, ноды — отдельный демон marzban-node.

За что его берут операторы:

  • инбаунды описываются сырым xray_config.json — доступно всё, что умеет Xray, без ограничений UI;
  • один пользователь = один токен подписки, который отдаёт все инбаунды и все ноды сразу;
  • вменяемый CLI и Telegram-бот из коробки, REST API для внешнего биллинга.

Честно о минусах, которые вылезут на второй неделе:

  • инбаунды общие для всех нод. Панель раздаёт один и тот же xray_config.json на каждую ноду. Штатно сделать «на ноде А Reality на 443, на ноде Б XHTTP на 8443» нельзя — только несколькими инбаундами с разными тегами и ручной раскладкой через Hosts. И если порт занят хотя бы на одной ноде, ядро на ней не стартует целиком.
  • активность апстрима низкая. По наблюдениям, репозиторий Gozargah/Marzban давно без крупных релизов, часть операторов ушла на Marzneshin, Remnawave или форки. Откройте репозиторий и посмотрите дату последнего коммита сами — это решение на год вперёд.
  • ядро в образе отстаёт. XHTTP, свежие фичи Reality и прочее требуют ручного апдейта Xray-core, см. раздел про эксплуатацию.
ЗадачаMarzbanКомментарий
100–3000 пользователей, 2–10 нодподходитштатный сценарий
Много тарифов, биллинг, партнёркачастичновнешний биллинг через REST API
Разные конфиги на разных нодахплохоконфиг общий для всех нод
Быстрый старт на одном сервереотлично15 минут до первого клиента

Сравнивайте панели до миграции данных, а не после: штатного экспорта пользователей между Marzban, Marzneshin и Remnawave нет, перенос всегда пишется руками через API.

Требования и установка Marzban

Требования скромные: Ubuntu 22.04/24.04 или Debian 12, публичный IP, домен с A-записью. Панель трафик не гоняет — 1 vCPU / 1 ГБ RAM хватает на пару тысяч пользователей на SQLite, узкое место появляется в базе, а не в CPU. Нагрузка ложится на ноды, где упор в канал и CPU.

Панель и рабочая нода на одном сервере — нормальная схема для старта, но в проде их разносят: если IP ноды заблокируют, админка с базой пользователей должна остаться доступной.

База:

apt update && apt -y upgrade
apt -y install curl socat git ufw jq
curl -fsSL https://get.docker.com | sh
docker --version && docker compose version
ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw enable

Установка Marzban официальным скриптом:

sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban.sh)" @ install

Скрипт кладёт docker-compose.yml и .env в /opt/marzban/, поднимает контейнер и ставит обёртку marzban в /usr/local/bin. Отрабатывает за 2–5 минут. В свежих версиях скрипта есть флаг --database mariadb — если он поддерживается в вашей версии, используйте сразу: миграция с SQLite позже стоит дороже (штатного мигратора нет, перенос пишется через API).

Если хотите контролировать всё руками, минимальный /opt/marzban/docker-compose.yml:

services:
 marzban:
 image: gozargah/marzban:v0.8.4 # тег, а не latest
 restart: always
 env_file:.env
 network_mode: host
 volumes:
 - /var/lib/marzban:/var/lib/marzban

Две детали. network_mode: host — не прихоть: инбаунды Xray слушают порты хоста напрямую, иначе придётся пробрасывать каждый порт руками. Конкретный тег вместо latest — ваш план отката: docker compose pull на latest может принести неожиданный образ, а с тегом откат к предыдущей версии занимает одну правку и marzban restart.

Что где лежит:

ПутьЧто это
/opt/marzban/.envвсе настройки панели
/opt/marzban/docker-compose.ymlописание сервисов
/var/lib/marzban/db.sqlite3база (если SQLite)
/var/lib/marzban/xray_config.jsonконфиг ядра, инбаунды/аутбаунды
/var/lib/marzban/certs/сертификаты, если кладёте их сюда
/var/lib/marzban/templates/кастомные шаблоны подписки

Команды обёртки: marzban up, marzban down, marzban restart, marzban logs -f, marzban update, marzban status, marzban cli.

Настройка Marzban: admin, .env и nginx с TLS

Сразу после старта создайте суперадмина:

marzban cli admin create --sudo

Скрипт спросит логин и пароль. Список: marzban cli admin list, смена пароля: marzban cli admin update -u <username>.

По умолчанию панель висит на http://IP:8000/dashboard/так оставлять нельзя: это открытая админка, пароль уходит по plaintext-HTTP, сканеры находят её за сутки. Правим /opt/marzban/.env:

UVICORN_HOST = "127.0.0.1"
UVICORN_PORT = 8000

DASHBOARD_PATH = "/aX7kd/"
XRAY_SUBSCRIPTION_URL_PREFIX = "https://sub.example.com"
XRAY_SUBSCRIPTION_PATH = "sub"

JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 1440
DOCS = false

# TELEGRAM_API_TOKEN = "123:AA..."
# TELEGRAM_ADMIN_ID = 123456789
ПеременнаяЗачем
UVICORN_HOST / UVICORN_PORTслушать только localhost, наружу — через nginx
DASHBOARD_PATHнестандартный путь админки, срезает почти весь фоновый скан
XRAY_SUBSCRIPTION_URL_PREFIXдомен, который попадёт в ссылку подписки клиенту
XRAY_SUBSCRIPTION_PATHпрефикс пути подписки (/sub/<token>)
SQLALCHEMY_DATABASE_URLсмена SQLite на MySQL/MariaDB
XRAY_EXECUTABLE_PATHпуть к своему бинарю Xray (обновление ядра)
DOCSSwagger на /docs, в проде выключить

После правки — marzban restart, затем проверка, что снаружи порт закрыт: ss -tlnp | grep 8000 должен показать 127.0.0.1:8000, а не 0.0.0.0:8000.

nginx-фронт (nginx 1.25+, listen 443 ssl http2 там уже deprecated):

server {
 listen 443 ssl;
 http2 on;
 server_name panel.example.com;

 ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
 ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;

 location / {
 proxy_pass http://127.0.0.1:8000;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection "upgrade";
 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;
 }
}

Для домена подписки — отдельный server с тем же proxy_pass плюс запрет кэша:

add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header Pragma "no-cache" always;

Это не паранойя. Клиенты на iOS охотно кэшируют ответ подписки и потом сутками ходят по старому списку серверов; без no-store вы будете каждому второму объяснять, почему «новый сервер не появился», а лечиться это будет удалением и повторным добавлением подписки.

Проверка обоих доменов:

curl -sI https://panel.example.com/aX7kd/ | head -1
curl -sI https://sub.example.com/sub/<token> | grep -i cache-control

Сертификаты — certbot --nginx или acme.sh, отдельно от Marzban. Учтите: порт 443 занят nginx, значит Reality-инбаунд на этой машине придётся ставить на другой порт (или, что правильнее, вынести ноду на отдельный сервер).

Первый инбаунд: xray_config.json и Hosts

UI-конструктора инбаундов в Marzban нет — вы правите xray_config.json руками. Через панель: Core Settings → редактор → Save & Restart Core. Через файл: /var/lib/marzban/xray_config.json + marzban restart.

Генерируем ключи Reality (имя контейнера уточните через docker ps):

docker exec -it marzban-marzban-1 xray x25519
docker exec -it marzban-marzban-1 openssl rand -hex 8 # shortId, 16 hex-символов

В сборках Xray-core 25.x вывод x25519 переименован: вместо пары Private key / Public key печатается PrivateKey / Password. Смысл тот же — первое значение идёт в privateKey конфига, второе клиенту в pbk. Сверяйтесь с выводом именно своей версии, не с чужой копипастой.

Минимальный рабочий конфиг:

{
 "log": { "loglevel": "warning" },
 "inbounds": [
 {
 "tag": "VLESS_REALITY",
 "listen": "0.0.0.0",
 "port": 443,
 "protocol": "vless",
 "settings": { "clients": [], "decryption": "none" },
 "streamSettings": {
 "network": "tcp",
 "security": "reality",
 "realitySettings": {
 "show": false,
 "dest": "www.samsung.com:443",
 "xver": 0,
 "serverNames": ["www.samsung.com"],
 "privateKey": "PRIVATE_KEY_ИЗ_x25519",
 "shortIds": ["", "6ba85179e30d4fc2"]
 }
 },
 "sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] }
 }
 ],
 "outbounds": [
 { "protocol": "freedom", "tag": "DIRECT" },
 { "protocol": "blackhole", "tag": "BLOCK" }
 ]
}

Выбор dest/serverNames под Reality — отдельная тема, здесь важны три правила, на которых спотыкаются все:

  1. clients оставляем пустым. Пользователей вписывает сама панель через Xray API. Ручные записи будут затёрты при рестарте ядра.
  2. tag уникален и осмыслен. Именно теги вы потом отмечаете галочками при создании пользователя.
  3. Ошибка в JSON = ядро не стартует, а панель при этом живая. Проверяйте до рестарта:
cp /var/lib/marzban/xray_config.json /root/xray_config.$(date +%s).json
python3 -m json.tool /var/lib/marzban/xray_config.json > /dev/null && echo JSON-OK
docker exec marzban-marzban-1 xray -test -c /var/lib/marzban/xray_config.json

Первая команда — ваш откат: вернуть файл и сделать marzban restart занимает 10 секунд.

Дальше Hosts (Host Settings). Инбаунд описывает, что слушает сервер; Host описывает, что попадёт в ссылку подписки: адрес, порт, SNI, Host-заголовок, ALPN, fingerprint, remark. Один инбаунд может иметь несколько хостов — это ваш штатный способ отдать пользователю и прямой адрес, и адрес за CDN одновременно.

В remark работают подстановки {USERNAME}, {SERVER_IP}, {DATA_USAGE} — клиент видит остаток трафика прямо в названии сервера.

Проверка: создайте тестового пользователя, откройте страницу подписки, скопируйте vless-ссылку в клиент. Не подключается — сначала marzban logs -f, потом уже клиент.

Подключение ноды: marzban-node

Нода — отдельный сервер, на котором крутится только Xray под управлением панели. Панель ходит к ноде на два порта: 62050 (сервис/подключение) и 62051 (Xray API).

Шаг 1 — на панели. Nodes → Add New Node: имя, адрес ноды, порты 62050/62051, галка «Add this node as a new host», если хотите, чтобы адрес сразу попал в подписки. Панель показывает клиентский сертификат — скопируйте его целиком, вместе со строками -----BEGIN/-----END.

Шаг 2 — на сервере ноды:

curl -fsSL https://get.docker.com | sh
mkdir -p /var/lib/marzban-node
nano /var/lib/marzban-node/ssl_client_cert.pem # вставляем сертификат из панели
mkdir -p /opt/marzban-node && cd /opt/marzban-node

docker-compose.yml:

services:
 marzban-node:
 image: gozargah/marzban-node:latest
 restart: always
 network_mode: host
 environment:
 SSL_CLIENT_CERT_FILE: "/var/lib/marzban-node/ssl_client_cert.pem"
 SERVICE_PROTOCOL: "rest"
 SERVICE_PORT: "62050"
 XRAY_API_PORT: "62051"
 volumes:
 - /var/lib/marzban-node:/var/lib/marzban-node
docker compose up -d && docker compose logs -f

Фаервол на ноде: 62050/62051 открываем только для IP панели, порты инбаундов — всем.

ufw allow from <IP_ПАНЕЛИ> to any port 62050 proto tcp
ufw allow from <IP_ПАНЕЛИ> to any port 62051 proto tcp
ufw allow 443/tcp

Отдельно: если на ноде стоит Docker с проброшенными портами, ufw их не закрывает — правила Docker в iptables идут раньше, и «закрытый» порт останется доступным снаружи. Проверяйте фактическую доступность снаружи (nmap/nc с другой машины), а не только вывод ufw status.

Чек-лист диагностики, когда нода висит в статусе «connecting»/«error»:

СимптомПричинаЧто делать
connection refusedконтейнер не поднялся / порт занятdocker compose logs, `ss -tlnp \grep 6205`
Таймаут на 62050фаервол или неверный IPnc -zv <node_ip> 62050 с панели
Ошибка TLS/handshakeсертификат скопирован не полностьюперезалить PEM целиком, включая BEGIN/END
Нода подключилась, трафика нетпорт инбаунда занят на нодеосвободить 443 (часто это nginx)
Постоянные реконнектынесовпадение SERVICE_PROTOCOLпривести rest/rpyc к одному значению с панелью

Помните про общий конфиг: как только нода подключилась, она получает тот же xray_config.json, что и панель. Если инбаунд слушает 443, а на ноде этот порт занят — ядро на ноде не стартует, в логах будет address already in use, и отвалятся все инбаунды этой ноды разом, а не один.

Эксплуатация: база, бэкапы, обновление ядра

База. SQLite по наблюдениям нормально живёт до нескольких сотен активных пользователей, дальше начинаются database is locked и подвисания панели в моменты сбора статистики. Переезд на MariaDB — сервис в compose плюс строка в .env:

SQLALCHEMY_DATABASE_URL = "mysql+pymysql://marzban:СИЛЬНЫЙ_ПАРОЛЬ@127.0.0.1:3306/marzban"

Штатного мигратора SQLite → MySQL в панели нет, перенос делается дампом с правкой схемы или выгрузкой-загрузкой через API. Поэтому решайте до набора пользователей, а не после.

Бэкап. Два пути — конфиг и данные:

tar czf /root/marzban-$(date +%F).tar.gz \
 /opt/marzban/.env /opt/marzban/docker-compose.yml /var/lib/marzban

Для MySQL добавьте mysqldump. В marzban-scripts есть команды marzban backup и marzban backup-service (отправка архива в Telegram) — набор команд менялся между версиями, проверьте наличие в своей: marzban --help.

Обновление панели: marzban update (внутри docker compose pull && up -d). Порядок безопасного апдейта: бэкап → зафиксировать текущий тег образа (docker inspect) → обновиться → проверить marzban logs -f и вход в админку. Откат — вернуть прежний тег в docker-compose.yml и marzban restart.

Обновление Xray-core. Образ Marzban несёт своё ядро, и оно нередко отстаёт на месяцы. Свой бинарь:

XRAY_EXECUTABLE_PATH = "/var/lib/marzban/xray-core/xray"
XRAY_ASSETS_PATH = "/var/lib/marzban/xray-core"
marzban core-update # либо руками: распаковать релиз Xray-core в /var/lib/marzban/xray-core/
marzban restart
docker exec marzban-marzban-1 /var/lib/marzban/xray-core/xray version

Критично: ядро обновляется отдельно на панели и отдельно на каждой ноде (на ноде — /var/lib/marzban-node/). Разные версии Xray на панели и ноде дают самые мутные баги: конфиг валиден на одной стороне и падает на другой. Держите версии одинаковыми и записывайте их куда-нибудь — при разборе инцидента это первый вопрос.

XHTTP. Транспорт требует свежего ядра на всех нодах. Два места, где ломается совместимость:

  • объект extra в xhttp на ноде и в клиентском конфиге должен совпадать — при расхождении соединение либо не встаёт, либо рвётся после первых пакетов. Отсюда классика «в одном приложении работает, в другом нет»;
  • экзотические obfuscation-поля из свежих сборок ломают совместимость между версиями Xray и клиентами. Не тащите их в прод без теста на всех клиентах, которыми реально пользуются ваши люди.

В стоковом Marzban поля для extra в Hosts может не быть — тогда либо кастомный шаблон подписки в /var/lib/marzban/templates/, либо форк с поддержкой XHTTP. Проверьте это до того, как обещать пользователям новый транспорт.

Когда IP ноды заблокировали. Смена IP лечит симптом: новый адрес живёт до следующей волны. Устойчивее убрать из подписки прямой адрес ноды и поставить перед ней фронт на «белом» домене — так работает whitelist-CDN как сервис: нода остаётся на месте, клиент ходит на адрес, который у операторов связи в белых списках. В Marzban это правится на уровне Hosts: тому же инбаунду добавляется второй host с CDN-адресом и нужными SNI/Host. Один технический факт, который стоит знать заранее: у Yandex CDN нет POST, поэтому XHTTP-аплинк идёт через GET, а GET разрешён только при явном mode: packet-up — без него получите «подключается, но не грузит». Полный разбор схемы «Marzban за CDN» — в отдельном материале, здесь достаточно понимать, что менять придётся Hosts и транспорт, а не панель.

Типовые ошибки при установке и настройке Marzban

Собрано по граблям, на которые операторы наступают в первую неделю.

ОшибкаПроявлениеРешение
Панель наружу на 0.0.0.0:8000 по HTTPбрутфорс и сканеры в логах через суткиUVICORN_HOST = "127.0.0.1" + nginx + TLS + DASHBOARD_PATH
XRAY_SUBSCRIPTION_URL_PREFIX не заданв подписке ссылка на http://IP:8000прописать домен, marzban restart, пересоздать ссылки
Битый JSON в xray_config.jsonпанель жива, ядро не стартует, клиенты отвалилисьpython3 -m json.tool и xray -test -c до рестарта, смотреть marzban logs -f
Клиенты вписаны в clients рукамиUUID пропадают после рестарта ядрапользователей заводить только через панель/API
443 занят nginx на той же машинеинбаунд не поднимается, address already in useразвести панель и ноду по серверам или сменить порт инбаунда
Сертификат ноды скопирован частичнонода в статусе error, TLS-ошибки в логахперезалить PEM целиком с BEGIN/END
Разные версии Xray на панели и нодахконфиг работает не везде, странные разрывыcore-update синхронно на всех хостах
ufw не закрыл 62050/62051порты видны снаружи, хотя в ufw status denyпроверять доступность снаружи; правила Docker идут раньше ufw
Нет no-store на домене подписки«у меня не появился новый сервер»заголовки no-cache в nginx, iOS-клиенты кэшируют агрессивно
SQLite при 500+ активныхтормоза, database is lockedMariaDB/MySQL, желательно до набора базы
Нет бэкапа .envпосле переустановки не совпадают токены и пути подписокбэкапить /opt/marzban/.env вместе с /var/lib/marzban

Отдельно про дисциплину: не выкатывайте изменения xray_config.json на живой панели без плана отката. Минимум — cp конфига с меткой времени перед каждой правкой и заранее записанная команда возврата. Откат занимает 10 секунд, а восстановление доверия пользователей после часа простоя — недели.

Частые вопросы

Как установить Marzban одной командой?

sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban.sh)" @ install на чистой Ubuntu 22.04/24.04 или Debian 12 с установленным Docker. Скрипт за 2–5 минут создаёт /opt/marzban/ с .env и docker-compose.yml, данные кладёт в /var/lib/marzban/, ставит обёртку marzban в /usr/local/bin. Сразу после установки обязательны два шага: marzban cli admin create --sudo и закрытие панели за nginx с TLS — по умолчанию она висит на 0.0.0.0:8000 без шифрования.

Где хранятся настройки Marzban и что бэкапить?

Настройки панели — /opt/marzban/.env, описание сервисов — /opt/marzban/docker-compose.yml, данные (база db.sqlite3, xray_config.json, шаблоны, сертификаты) — /var/lib/marzban/. Бэкап: tar czf backup.tar.gz /opt/marzban/.env /opt/marzban/docker-compose.yml /var/lib/marzban. Если база на MySQL/MariaDB — плюс mysqldump. Без .env восстановление неполное: там ключи и префиксы, от которых зависят рабочие ссылки подписок.

Почему нода Marzban не подключается к панели?

Порядок проверки: 1) docker compose logs -f на ноде — стартовал ли контейнер; 2) nc -zv <node_ip> 62050 с панели — проходит ли сеть, порты 62050/62051 должны быть открыты для IP панели; 3) сертификат в /var/lib/marzban-node/ssl_client_cert.pem скопирован целиком, вместе со строками BEGIN/END; 4) SERVICE_PROTOCOL (rest/rpyc) одинаков на панели и ноде. Если нода подключилась, но трафика нет — почти всегда порт инбаунда занят на ноде другим процессом, в логах address already in use.

Можно ли на разных нодах Marzban держать разные инбаунды?

Штатно нет: панель раздаёт один общий xray_config.json всем нодам. Частичный обход — несколько инбаундов с разными тегами и портами, а через Hosts раскладывать нужные адреса на нужные теги, чтобы клиент получал только релевантные ссылки. Но конфиг физически остаётся на всех нодах, и если порт занят хотя бы на одной — на ней не поднимется ядро целиком. Если разные конфигурации по нодам нужны как базовое требование, Marzban для этой задачи неудачный выбор.

Как обновить Xray-core в Marzban для поддержки XHTTP?

Пропишите в .env XRAY_EXECUTABLE_PATH = "/var/lib/marzban/xray-core/xray" и XRAY_ASSETS_PATH = "/var/lib/marzban/xray-core", затем marzban core-update (или вручную распакуйте релиз Xray-core в этот каталог) и marzban restart. Проверьте фактическую версию: docker exec marzban-marzban-1 /var/lib/marzban/xray-core/xray version. То же самое повторите на каждой ноде отдельно — образ ноды несёт своё ядро. Версии на панели и нодах должны совпадать, иначе получите конфиг, валидный на одной стороне и падающий на другой.

Что делать, когда IP ноды заблокировали?

Смена IP помогает до следующей волны — это лечение симптома. Устойчивее убрать из подписки прямой адрес ноды и поставить перед ней фронт на «белом» домене (whitelist-CDN): нода остаётся на месте, клиенты ходят на адрес, который у операторов не режется. В Marzban это правится на уровне Hosts — тому же инбаунду добавляется второй host с CDN-адресом и корректными SNI/Host, старый host можно оставить как запасной. Для XHTTP за Yandex CDN учтите: POST там не поддерживается, аплинк идёт GET, а GET работает только при явном mode: packet-up.

Marzban ещё развивается или пора смотреть альтернативы?

По наблюдениям, апстрим Gozargah/Marzban давно без крупных релизов, часть операторов перешла на Marzneshin, Remnawave или форки. Панель при этом рабочая и стабильная — тысячи инсталляций в проде. Практический совет: перед установкой откройте репозиторий и посмотрите дату последнего коммита и релиза своими глазами. Если панель выбирается на годы и вы рассчитываете на свежие транспорты из коробки, закладывайте либо форк с активной поддержкой, либо ручное обновление ядра как часть регламента эксплуатации.

Whitelisted-вход для вашего VPN-сервиса

Clearway даёт «белый» CDN-вход перед вашей нодой — устойчивый к троттлингу операторов. Первые 10 ГБ бесплатно, без карты.

Попробовать →