Архитектура: где проходит настоящая граница
Все три панели решают одну задачу — управляют пользователями Xray-core и раздают подписки. Сравнение панелей VPN осмысленно начинать не с интерфейса, а с того, что вам придётся эксплуатировать в 3 часа ночи.
3x-ui (форк MHSanaei от x-ui) — один Go-бинарник + SQLite. Панель и Xray на одном сервере, панель сама пишет конфиг и дёргает Xray API. Кроме systemd не нужно ничего.
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
x-ui # меню: порт панели, basePath, логин, SSL
ls -la /etc/x-ui/x-ui.db # вся база сервиса — один файл
Marzban — Python/FastAPI в Docker, SQLite по умолчанию или MySQL/MariaDB. Панель отделена от ядра: локальный Xray плюс сколько угодно marzban-node по TLS с клиентскими сертификатами.
sudo bash -c "$(curl -sL https://github.com/Gozargah/Marzban-scripts/raw/master/marzban.sh)" @ install
marzban cli admin create --sudo
# конфиг: /opt/marzban/.env, docker-compose.yml
# данные: /var/lib/marzban/ (xray_config.json, templates/, db)
Remnawave — TypeScript/NestJS, обязательные PostgreSQL и Valkey/Redis, отдельный контейнер страницы подписки, агент remnanode на каждом сервере. После docker compose up -d вы увидите не один процесс, а четыре-пять контейнеров (backend, db, valkey, subscription-page) плюс внешний reverse proxy — панель наружу сама не смотрит. Пошаговая установка разобрана у нас в отдельном материале, здесь важен сам факт: это распределённая система, а не «программа на сервере».
Практический вывод: 3x-ui — «панель = сервер». Marzban и Remnawave — «панель = центр управления, серверы = ноды». Это и есть главная развилка.
Мультинода: где 3x-ui упирается в потолок
У 3x-ui мультиноды нет. Совсем. Пять серверов — это пять независимых панелей, пять баз, пять раз руками завести клиента. Обходные пути существуют (склеить подписку своим скриптом, продублировать inbound с теми же UUID), но это самодельный костыль, который чинить будете вы.
Marzban: нода — отдельный контейнер, панель ходит на два порта.
# /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"
volumes:
- /var/lib/marzban-node:/var/lib/marzban-node
62050 (сервисный) и 62051 (Xray API) открываем только для IP панели — это не паранойя, открытый 62051 отдаёт статистику и управление ядром:
ufw allow from <IP_ПАНЕЛИ> to any port 62050,62051 proto tcp
Важно: если Xray на ноде запущен в Docker с network_mode: host, ufw его не закрывает при обычной установке — правила для контейнерных портов пишутся в цепочку DOCKER-USER. Проверяйте iptables -L DOCKER-USER -n после настройки, а не только ufw status.
Remnawave: агент получает сертификат панели одной переменной и слушает 2222 (наружу выставлять не нужно).
# /opt/remnanode/.env
APP_PORT=2222
SSL_CERT="eyJub2RlQ2VydFBlbSI6..." # копируется из панели при создании ноды
Честное сравнение: у Marzban мультинода проверена годами и стабильна, но xray_config.json один на все ноды — разные транспорты на разных серверах делаются тегами и исключениями, это неудобно и легко разъезжается. В Remnawave профиль конфигурации назначается по нодам, а пользователи привязаны к сквадам (в 2.x — internal squads, раньше это были просто inbounds) — гибче и меньше ручной синхронизации.
Подписки и клиенты: где сервис ломается на практике
Подписка — это то, что видит клиент, и оттуда приходит большая часть тикетов.
3x-ui. Встроенный subscription-сервис на отдельном порту (по умолчанию 2096), два эндпоинта: /{subPath}/{id} — base64-список, /{subJsonPath}/{id} — JSON для клиентов, которые его понимают. Настраивается в Panel Settings → Subscription. Заголовки profile-title и subscription-userinfo панель проставляет сама, шаблонов под конкретные приложения нет.
Marzban. Самая гибкая шаблонизация из трёх: Jinja2 в /var/lib/marzban/templates/ и выдача по User-Agent — Clash получает YAML, sing-box получает JSON, остальным уходит base64.
# /opt/marzban/.env
CUSTOM_TEMPLATES_DIRECTORY="/var/lib/marzban/templates/"
SUBSCRIPTION_PAGE_TEMPLATE="subscription/index.html"
CLASH_SUBSCRIPTION_TEMPLATE="clash/default.yml"
SUB_PROFILE_TITLE="MyService"
SUB_UPDATE_INTERVAL="6"
Remnawave. Страница подписки — отдельный сервис (remnawave-subscription-page, по умолчанию 3010) с JSON-шаблонами под типы клиентов, глубокой интеграцией с Happ (deeplink, роутинг, шифрование ссылки) и HWID-лимитом устройств. Это единственная из трёх, где «не больше N устройств» встроено, а не прикручено сбоку.
Общая боль, от панели не зависящая: клиенты кэшируют подписку. По наблюдениям, iOS-приложения держат старый список часами после смены нод. Первое, что надо сделать на фронте:
location /sub/ {
proxy_pass http://127.0.0.1:3010; # remnawave-subscription-page; для 3x-ui — 2096
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
proxy_hide_header Etag;
}
Если и после этого клиент показывает старые серверы — помогает только переподписка: удалить ссылку и добавить заново. Это поведение приложения, а не баг панели.
API, боты и биллинг: чем автоматизировать
Встроенного биллинга нет ни в одной из трёх. Приём платежей, тарифы, промокоды, продления — всегда сторонний бот, который дёргает API. Разница в том, насколько удобно его дёргать.
Marzban — OpenAPI-схема на /docs, токен по паролю админа, объект первого класса — пользователь:
TOKEN=$(curl -s -X POST 'https://panel.example.com/api/admin/token' \
-d 'username=admin&password=***' | jq -r.access_token)
curl -s -X POST 'https://panel.example.com/api/user' \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"username":"user_1042","proxies":{"vless":{"flow":""}},
"inbounds":{"vless":["VLESS-XHTTP"]},
"data_limit":107374182400,"expire":1790000000,"status":"active"}'
Remnawave — API-first: всё, что делает веб-морда, доступно снаружи. Токен создаётся в панели, дальше POST /api/users с username, trafficLimitBytes, expireAt, activeInternalSquads, плюс вебхуки на события (истёк, превысил лимит) — главный аргумент, если вы пишете свой шоп-бот. Оговорка из практики: если панель прикрыта дополнительной защитой на реверс-прокси (кастомный заголовок или cookie-авторизация), бот обязан слать её тоже, иначе получите 403 при рабочих кредах — это самая частая «непонятная» поломка интеграции.
3x-ui — API есть, но он вторичен: POST /login за cookie, дальше /panel/api/inbounds/list и /panel/api/inbounds/addClient, где клиент передаётся строкой JSON внутри JSON. Работает, но писать биллинг поверх больно: вы оперируете inbound'ами, а не пользователями, и любое изменение конфига требует переписывать settings целиком.
Готовые магазины-боты: под Marzban их исторически больше (наследие большого сообщества), под Remnawave экосистема моложе, но растёт быстрее и почти вся на актуальном API. Под 3x-ui есть встроенный телеграм-бот самой панели — он про уведомления и бэкапы, а не про продажи.
Сводная таблица: 3x-ui vs Marzban vs Remnawave
Данные — на лето 2026, перед установкой сверяйтесь с актуальными релизами; требования по RAM — по наблюдениям на боевых установках, а не из документации.
| Критерий | 3x-ui | Marzban | Remnawave |
|---|---|---|---|
| Стек | Go, один бинарник | Python/FastAPI, Docker | TypeScript/NestJS, Docker |
| База | SQLite | SQLite / MySQL / MariaDB | PostgreSQL + Valkey |
| RAM панели | от 512 МБ | от 1 ГБ | от ~2 ГБ |
| Мультинода | нет | да (62050/62051) | да (remnanode, 2222) |
| Разные конфиги по нодам | н/д | плохо: один xray_config | да, профили конфигураций |
| Шаблоны подписок | минимум | Jinja2 по User-Agent | JSON-шаблоны, Happ |
| Лимит устройств (HWID) | нет | нет | встроен |
| API под биллинг | есть, неудобный | хороший, OpenAPI | лучший, вебхуки |
| Телеграм-бот | встроен (алерты, бэкап) | внешние | внешние |
| Темп разработки | активный | замедлился | быстрый, breaking changes |
| Порог входа | низкий | средний | высокий |
| Кому | 1 сервер, старт | 2–10 нод, работающий сервис | сервис на вырост |
Чего обычно не учитывают в спорах «сравнение панелей VPN» — цену ошибки. 3x-ui теряется вместе с сервером, но и восстанавливается копированием одного файла. Remnawave требует уважения к миграциям: docker compose pull на смене major-версии без дампа Postgres — прямой путь к простою, а откат назад по схеме БД не предусмотрен.
Эксплуатация: бэкапы, обновления, миграция
О чём не думают при выборе и о чём жалеют через полгода.
3x-ui — бэкап это копия файла:
systemctl stop x-ui
cp /etc/x-ui/x-ui.db /root/backup/x-ui-$(date +%F).db
systemctl start x-ui
По наблюдениям, SQLite начинает подтормаживать на нескольких сотнях активных клиентов с частым обновлением статистики трафика — это сигнал, что пора менять панель, а не тюнить её.
Marzban — забираем и конфиги, и данные:
tar czf /root/marzban-$(date +%F).tar.gz /opt/marzban /var/lib/marzban
marzban restart && marzban logs -f
При тысяче с лишним пользователей переезжайте с SQLite на MariaDB (SQLALCHEMY_DATABASE_URL в .env), иначе поймаете database is locked на записи статистики.
Remnawave — только логический дамп, копии volume недостаточно:
docker ps --format '{{.Names}}' | grep db # уточните имя контейнера
docker compose exec -T remnawave-db \
pg_dump -U postgres -d postgres > /root/remnawave-$(date +%F).sql
Правило простое: дамп до docker compose pull, всегда, и читать CHANGELOG перед сменой минорной версии — там регулярно бывают несовместимые изменения API и схемы.
Миграция. Community-скрипты Marzban → Remnawave существуют и в целом переносят пользователей, UUID, лимиты и даты. Официальной поддержки у них нет, поэтому схема одна: поднять Remnawave параллельно, перелить пользователей, выдать новые ссылки-подписки и держать старую панель живой хотя бы неделю. Ссылки при переезде меняются в любом случае — домен и формат страницы подписки другие. Обратной миграции (Remnawave → Marzban) практически нет, и это стоит считать фактором блокировки выбора.
Чего не решает ни одна панель: заблокированный IP ноды
Панель управляет пользователями и подписками. К доступности IP она отношения не имеет: когда ТСПУ выкашивает адрес ноды, ни 3x-ui, ни Marzban, ни Remnawave не помогут — нужен вход на «белом» домене, чтобы клиент ходил на whitelist-адрес, а тот уже — на вашу ноду. Этим и занимается Clearway: CDN-фронт для нод операторов; техника ниже применима и без него.
Один техфакт, который экономит часы отладки: у Yandex CDN нет POST, поэтому аплинк XHTTP уходит методом GET, а GET разрешён только в режиме packet-up. Если mode не задан явно, соединение либо не встаёт, либо рвётся на первом крупном запросе — отсюда классическое «в одном приложении работает, в другом нет». Минимальный инбаунд:
{
"tag": "xhttp-cdn",
"listen": "127.0.0.1",
"port": 8443,
"protocol": "vless",
"settings": { "clients": [], "decryption": "none" },
"streamSettings": {
"network": "xhttp",
"security": "none",
"xhttpSettings": {
"host": "front.example.com",
"path": "/tunnel",
"mode": "packet-up"
}
}
}
Куда класть: 3x-ui — вкладка Inbounds, транспорт xhttp; Marzban — /var/lib/marzban/xray_config.json или редактор в панели, затем marzban restart; Remnawave — профиль конфигурации ноды. Детальный разбор транспорта, TLS на фронте и защиты origin у нас в отдельных материалах про XHTTP через CDN.
Два правила ломают больше всего установок. Первое: extra на ноде и в клиенте должны совпадать — панель генерирует ссылку из своего представления конфига, и если вы правили xhttpSettings руками мимо панели, клиент получит не тот extra и соединение не встанет. Второе: не добавляйте экзотические obfuscation-поля — они ломают совместимость между версиями Xray, нода на свежем ядре и клиент полугодовой давности просто молча не договорятся. Минимальный набор полей живёт дольше.