Что такое 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 (обновление ядра) |
DOCS | Swagger на /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 — отдельная тема, здесь важны три правила, на которых спотыкаются все:
clientsоставляем пустым. Пользователей вписывает сама панель через Xray API. Ручные записи будут затёрты при рестарте ядра.tagуникален и осмыслен. Именно теги вы потом отмечаете галочками при создании пользователя.- Ошибка в 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 | фаервол или неверный IP | nc -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 locked | MariaDB/MySQL, желательно до набора базы |
Нет бэкапа .env | после переустановки не совпадают токены и пути подписок | бэкапить /opt/marzban/.env вместе с /var/lib/marzban |
Отдельно про дисциплину: не выкатывайте изменения xray_config.json на живой панели без плана отката. Минимум — cp конфига с меткой времени перед каждой правкой и заранее записанная команда возврата. Откат занимает 10 секунд, а восстановление доверия пользователей после часа простоя — недели.