v2603 · server-manager

server-manager

Модульная система установки и управления VPN-инфраструктурой. Remnawave + Hysteria2 + MTProxy — один инструмент.

$ curl -fsSL https://raw.githubusercontent.com/stump3/server-manager/main/server-manager.sh | bash
🗺
Обзор

Три независимых VPN-компонента под одним скриптом. Каждый устанавливается, управляется и мигрирует отдельно — или все вместе.

🛡️
Remnawave Panel
VPN-панель на eGames архитектуре. Xray/Reality, cookie-защита входа, Docker Compose.
📡
MTProxy
Telegram MTProto прокси на Rust. systemd или Docker, hot reload без разрыва соединений.
🚀
Hysteria2
Высокоскоростной VPN поверх QUIC/UDP. Эффективен при потерях пакетов и нестабильном канале.
📦
Перенос
Миграция любого компонента или всего стека на новый сервер через SSH + пароль.
Быстрый старт
1
Подключитесь по SSH
bash
ssh root@YOUR_SERVER_IP
2
Скачайте и запустите скрипт
bash
curl -fsSL https://raw.githubusercontent.com/stump3/server-manager/main/server-manager.sh | bash
3
Выберите компонент в меню

Скрипт покажет статусы и предложит список действий. Каждый компонент устанавливается независимо.

🛡️
Remnawave Panel

VPN-панель управления. Архитектура eGames: nginx в network_mode: host, Xray (remnanode) принимает Reality-трафик на порту 443. Доступ в панель защищён cookie-ключом.

Подменю

Remnawave Panel
1) 🔧 Установка
2) ⚙️ Управление
3) 🌐 WARP Native
4) 🎨 Страница подписки
5) 🖼️ Selfsteal шаблон
6) 📦 Миграция на другой сервер
7) 🗑️ Удалить панель
0) ◀️ Назад

Параметры установки

ПараметрПримерОписание
Режим1 / 21 — панель + нода, 2 — только панель
Домен панелиpanel.example.comОсновной домен
Домен подписокsub.example.comДля клиентских конфигов
Домен selfstealnode.example.comДля Reality
Метод SSL1 / 2 / 3Cloudflare / ACME standalone / Gcore
Логин суперадминаавтоСлучайные 8 букв [a-zA-Z]{8}, генерируется автоматически
Пароль суперадминаавтоСлучайный, генерируется автоматически
🔐
Сохраните логин, пароль и URL с cookie-ключом. Логин и пароль генерируются автоматически — восстановить их нельзя. Без cookie-URL войти в панель невозможно.

https://panel.example.com/auth/login?KEY=VAL

Как работает cookie-защита

При установке генерируются два случайных слова — они вшиваются в nginx.conf как имя и значение cookie:

KEY и VAL — это [a-zA-Z]{8}, два независимых случайных слова. Nginx проверяет или наличие cookie, или наличие query-параметра — достаточно одного.

nginx
# nginx.conf — как это выглядит внутри
map $http_cookie $auth_cookie {
    default 0;
    "~*xKtBpWnR=mQaYjZvL" 1;   # авторизован по cookie
}
map $arg_xKtBpWnR $auth_query {
    default 0;
    "mQaYjZvL" 1;               # авторизован по query
}
map "$auth_cookie$auth_query" $authorized {
    "~1" 1;   # хотя бы одно совпало
    default 0;
}
💡
Если URL потерян — откройте прямой доступ командой rp open_port, затем восстановите KEY и VAL из конфига:
grep -A2 "map \$arg_" /opt/remnawave/nginx.conf | head -4

Команды управления rp

rpИнтерактивное меню
rp statusСтатус контейнеров
rp logs [svc]Логи: panel / nginx / sub / node
rp restart [svc]Перезапуск: all / nginx / panel / sub / node
rp sslОбновить SSL-сертификат
rp backupБэкап БД и конфигов
rp healthДиагностика сервисов
rp open_portОткрыть порт 8443 (восстановление)
rp close_portЗакрыть порт 8443
rp migrateПеренос на другой сервер
📡
MTProxy (telemt)

Telegram MTProto прокси на Rust. Поддерживает ограничения на пользователей, hot reload без разрыва соединений.

ℹ️
При входе в раздел MTProxy — если сервис уже запущен, режим (systemd/Docker) определяется автоматически, меню выбора пропускается.

Подменю

MTProxy
1) Установить
2) Добавить пользователя
3) Пользователи и ссылки
4) Статус и логи
5) Обновить
6) Остановить
7) Мигрировать на новый сервер (только systemd)
8) Сменить режим (systemd ↔ Docker)
0) Назад

Режимы запуска

РежимПлюсыКогда выбирать
systemd рекомендуетсяHot reload, меньше RAM, миграцияПо умолчанию
DockerИзоляция, простое обновлениеЕсли уже используете Docker-стек

Параметры установки

ПараметрПо умолчаниюОписание
Порт8443Telegram: 443, 8443, 2053, 2083, 2087, 2096
Домен-маскировкаpetrovich.ruЛюбой крупный HTTPS-сайт
Имя пользователяМинимум один при установке
Секретавто32 hex-символа

Ограничения на пользователей

ПараметрОписание
max_tcp_connsМаксимум одновременных подключений
max_unique_ipsМаксимум уникальных IP-адресов
data_quota_bytesКвота трафика в ГБ
expiration_rfc3339Срок действия в днях
Добавление пользователя применяется через hot reload — существующие соединения не прерываются.
🚀
Hysteria2

Высокоскоростной VPN поверх QUIC/UDP. Эффективен на нестабильных каналах и при высоких потерях пакетов.

⚠️
Требуется отдельный домен с A-записью на IP сервера (например cdn.example.com).
Порт 80/tcp — скрипт открывает автоматически перед получением сертификата и закрывает после.

Совместимость

КомпонентКонфликтРешение
Xray/Reality UDP 443⚠ естьВыбрать другой порт — скрипт проверит автоматически
nginx TCP 80/443частичноПорт 80 открывается временно только для ACME
MTProxy 2053/8443частичноСкрипт показывает занятые порты в меню
certbot / Let's EncryptнетHysteria использует собственный ACME-клиент

Параметры установки

ПараметрПо умолчаниюОписание
ДоменFQDN вида cdn.example.com с A-записью на сервер
EmailДля ACME-уведомлений, необязателен
CALet's EncryptЦентр сертификации
Порт8443UDP — скрипт проверяет занятость в реальном времени
ЛогинИмя пользователя (вводится вручную)
ПарольавтоГенерируется если не указан
НазваниеHysteria2Отображается в URI и клиенте (например: 🇩🇪 Germany Hysteria2)
Masqueradebing.comРежим маскировки трафика
АлгоритмBBRBBR или Brutal

Выбор CA

CAСрокКогда использовать
Let's Encrypt рекомендуется90 днейПо умолчанию
ZeroSSL90 днейЕсли Let's Encrypt заблокирован провайдером
Buypass180 днейЕсли нужен более долгий срок

Режимы маскировки

ВариантТипОписание
bing.com рекомендуетсяproxyПоддерживает HTTP/3
yahoo.comproxyСтабильный
cdn.apple.comproxyНейтральный
speed.hetzner.deproxyНейтральный
/var/www/htmlfileЛокальная заглушка Remnawave
Свой URLproxyЛюбой HTTP/3 сайт

Алгоритмы скорости

АлгоритмКогда использовать
BBR рекомендуетсяСтабильный канал — по умолчанию
BrutalНестабильный канал, потери >5%, мобильный интернет. Создаёт до 1.4× нагрузки — следите за трафиком на VPS с лимитом

Результат установки

URI
hy2://Login:[email protected]:8443?sni=cdn.example.com&alpn=h3&insecure=0#Germany Hysteria2

Файлы сохраняются в /root/:

ФайлСодержимое
hysteria-{домен}.txtURI подключения
hysteria-{домен}.yamlКонфиг Clash/Mihomo
hysteria-{домен}.pngQR-код PNG
hysteria-{домен}-users.txtURI всех добавленных пользователей

Подменю

Hysteria2
1) 🔧 Установка
2) ⚙️ Управление
3) 👥 Пользователи
4) 🔗 Подписка
5) 📦 Миграция на другой сервер

0) ◀️ Назад

Меню управления (пункт 2)

Hysteria2 — Управление
1) 📊 Статус
2) 📋 Логи
3) 🔄 Перезапустить
4) 📶 Bandwidth
5) 🎭 Маскировка

0) ◀️ Назад

Bandwidth

РежимКогда использоватьКак задать
BBR рекомендуется Стабильный канал. Автоподбор скорости — ничего настраивать не нужно Не задавать bandwidth блок
Brutal Нестабильный канал, потери >5%, мобильный интернет. Агрессивно держит заданную скорость Задать bandwidth.up и bandwidth.down

Указывайте скорость серверного канала, не клиентского. Завышение → перегрузка сети.

Скорость сервераЗначение
1 Гбит/с1 gbps
500 Мбит/с500 mbps
100 Мбит/с100 mbps
ℹ️
При правильно заданном bandwidth клиент подключается с tx: N вместо tx: 0. tx: 0 — признак того что bandwidth не задан или клиент его не передал.

Интеграция с Remnawave (пункт 4 → 3)

Webhook-сервис синхронизирует пользователей Hysteria2 с Remnawave в реальном времени. Форк subscription-page автоматически добавляет hy2:// URI в ответ подписки.

Hysteria2 → Подписка
1) 📤 Опубликовать подписку
2) 🔗 Объединить с подпиской Remnawave (merger)
3) 🪝 Интеграция с Remnawave (webhook + sub-page)

0) ◀️ Назад
📊
Учёт трафика Hysteria2

Трафик Hysteria2 агрегируется в БД Remnawave каждые 60 секунд. Панель показывает суммарное потребление — VLESS + Hysteria2 вместе. При отключении пользователя активные сессии разрываются мгновенно через /kick.

📍
Включается при установке Hysteria2, либо через: Hysteria2 → Подписка → Интеграция с Remnawave → Установить

Схема работы

flow
Hysteria2 trafficStats API (:9999)
    ↓  GET /traffic?clear=1  каждые 60с
    ↓  {username: {tx, rx}}  →  delta = tx + rx
hy-webhook traffic_poller (фоновый поток)
    ↓  _save_pending()  — буфер на случай краша
    ↓  psycopg2 → PostgreSQL :6767
    ↓  UPDATE user_traffic SET used_traffic_bytes += delta
    ↓  _delete_pending()
Remnawave Panel  — показывает обновлённый трафик

Remnawave user.deleted / disabled / expired
    ↓  handle_user_deleted() + POST /kick
    ↓  Hysteria2 разрывает сессии немедленно

Переменные окружения /etc/hy-webhook.env

ПеременнаяДефолтОписание
HY_TRAFFIC_PORT9999Порт trafficStats API
HY_TRAFFIC_SECRETСекрет для Authorization заголовка
DATABASE_URLDSN PostgreSQL. Пустой = поллер отключён
TRAFFIC_POLL_INTERVAL60Интервал опроса в секундах
⚠️
Если DATABASE_URL не задан — поллер завершается сразу: DATABASE_URL не задан — учёт трафика отключён. На основную работу не влияет.

Конфиг Hysteria2 — секция trafficStats

yaml
trafficStats:
  listen: 127.0.0.1:9999
  secret: <секрет>

Добавляется автоматически при установке Hysteria2. Секрет генерируется как openssl rand -hex 16 и сохраняется в /etc/hy-webhook.env.

Ручное включение

bash
# 1. Добавить секцию в config.yaml и перезапустить
printf '\ntrafficStats:\n  listen: 127.0.0.1:9999\n  secret: my-secret\n' \
    >> /etc/hysteria/config.yaml
systemctl restart hysteria-server

# 2. Установить psycopg2
pip3 install psycopg2-binary --break-system-packages

# 3. Добавить переменные и перезапустить hy-webhook
sed -i '/^HY_TRAFFIC_SECRET=/d;/^DATABASE_URL=/d;/^TRAFFIC_POLL_INTERVAL=/d' /etc/hy-webhook.env
printf 'HY_TRAFFIC_SECRET=my-secret\nHY_TRAFFIC_PORT=9999\nDATABASE_URL=postgresql://postgres:postgres@127.0.0.1:6767/postgres\nTRAFFIC_POLL_INTERVAL=60\n' >> /etc/hy-webhook.env
systemctl restart hy-webhook

# 4. Проверить логи
journalctl -u hy-webhook -f
# Ожидаемые строки:
# Traffic poller запущен, интервал 60с
# Traffic записан в БД: N пользователей

Полезные SQL-запросы

sql
-- Пользователи с трафиком
SELECT u.username, u.status,
       round(t.used_traffic_bytes / 1073741824.0, 2) AS used_gb,
       round(t.lifetime_used_traffic_bytes / 1073741824.0, 2) AS lifetime_gb
FROM users u JOIN user_traffic t ON u.t_id = t.t_id
ORDER BY t.used_traffic_bytes DESC;

-- Сбросить трафик пользователя
UPDATE user_traffic SET used_traffic_bytes = 0
WHERE t_id = (SELECT t_id FROM users WHERE username = 'admin');
📦
Перенос

Перенос сервисов на новый сервер через SSH + пароль. sshpass устанавливается автоматически.

Перенос
1) Перенести Remnawave Panel
2) Перенести MTProxy (telemt, только systemd)
3) Перенести Hysteria2
4) Перенести всё (Panel + MTProxy + Hysteria2)
0) Назад

Мониторинг DNS после переноса Hysteria2

После переноса скрипт предлагает дождаться обновления DNS и автоматически перезапустить:

DNS monitor
[ 1] cdn.example.com → 5.6.7.8 — ожидание......
[ 2] cdn.example.com → 5.6.7.8 — ожидание......
[ 3] cdn.example.com → 1.2.3.4

✅ DNS обновлён
✅ Сервис перезапущен — ACME переиздаст сертификат автоматически

Таймаут ожидания — 60 минут.

После переноса — остановить старые сервисы

bash
systemctl stop hysteria-server
systemctl stop telemt
cd /opt/remnawave && docker compose down
🔗
Selfsteal — архитектура
ℹ️
Важно: nginx НЕ слушает порт 443 в selfsteal режиме. Порт 443 принадлежит Xray, который форвардит трафик в unix-сокет nginx.

Схема трафика

Порядок запуска

1
docker compose up -d

nginx стартует и создаёт /dev/shm/nginx.sock

2
Remnawave регистрирует ноду

Нода (remnanode) подключается к панели на 172.30.0.1:2222

3
Xray получает конфиг и стартует

rw-core занимает :443, начинает Reality handshake и запись в nginx.sock

Диагностика

bash
# Кто слушает 443? Должен быть rw-core, НЕ nginx
ss -tlnp | grep :443

# Сокет существует?
ls -la /dev/shm/nginx.sock

# Xray запустился?
docker logs remnanode --tail=10 | grep -E "Xray started|SPAWN_ERROR"

# Конфиг содержит inbounds?
docker logs remnanode --tail=5 | grep "inbounds"
# OK: inbounds:[{"tag":"Steal",...}]
# ПРОБЛЕМА: inbounds:[]  ← нода не получила конфиг

Частые ошибки

СимптомПричинаРешение
SPAWN_ERROR: xraynginx занимает порт 443Убедиться что в nginx.conf нет listen 443 для selfsteal
inbounds:[]Нода без activeInboundsВ панели: Nodes → ред. → убедиться что inbound отмечен активным
nginx.sock отсутствуетnginx не стартовалdocker restart remnawave-nginx
Интеграция Hysteria2 → Remnawave

Добавляет hy2:// URI в подписку Remnawave автоматически — при создании, удалении или изменении пользователя.

📍
Путь в меню: 3) Hysteria2 → 4) Подписка → 3) Интеграция с Remnawave

Схема работы

flow
Remnawave (user.created / deleted / disabled)
    ↓  POST http://172.30.0.1:8766/webhook
    ↓  X-Remnawave-Signature: HMAC-SHA256
hy-webhook (systemd, :8766, 0.0.0.0)
    ↓  verify → sanitize → gen_password → update users.json
    ↓  update /etc/hysteria/config.yaml
    ↓  systemctl reload-or-restart hysteria-server
subscription-page (remnawave-sub-hy:local)
    ↓  при запросе подписки читает users.json
    ↓  добавляет hy2:// URI в base64 ответ

Требования

УсловиеПроверка
/opt/remnawave/Remnawave установлена через server-manager
/etc/hysteria/config.yamlHysteria2 установлена через server-manager
UFW: 172.16.0.0/12 → 8766Добавляется автоматически при установке
REMNAWAVE_API_TOKENСоздать в панели: Settings → API Tokens

Настройка webhook в .env

.env
WEBHOOK_ENABLED=true
WEBHOOK_URL=http://172.30.0.1:8766/webhook   # НЕ 127.0.0.1!
WEBHOOK_SECRET_HEADER=<hex64>
⚠️
WEBHOOK_URL должен быть http://172.30.0.1:8766 — gateway Docker сети. Адрес 127.0.0.1 из контейнера указывает на сам контейнер, не на хост.

Диагностика интеграции

bash
# Статус webhook
curl -s http://127.0.0.1:8766/health

# Доступность из контейнера
docker exec remnawave curl -s http://172.30.0.1:8766/health

# Логи событий
journalctl -u hy-webhook -n 20

# Проверить hy2:// в подписке
curl -s "https://sub.domain/TOKEN" | base64 -d | grep "hy2://"
📁
Файлы и пути

Hysteria2

ПутьНазначение
/etc/hysteria/config.yamlКонфигурация сервера
/usr/local/bin/hysteriaБинарник
/root/hysteria-{домен}.txtURI подключения
/root/hysteria-{домен}.yamlКонфиг Clash/Mihomo
/root/hysteria-{домен}.pngQR-код PNG
/root/hysteria-{домен}-users.txtURI всех пользователей

Remnawave Panel

ПутьНазначение
/opt/remnawave/.envКонфигурация панели
/opt/remnawave/docker-compose.ymlDocker Compose
/opt/remnawave/nginx.confNginx (cookie-ключ здесь)
/opt/remnawave/backups/Бэкапы
/usr/local/bin/remnawave_panelСкрипт управления rp

MTProxy (systemd)

ПутьНазначение
/usr/local/bin/telemtБинарник
/etc/telemt/telemt.tomlКонфиг
/etc/systemd/system/telemt.serviceSystemd-сервис
⌨️
Ручное управление

Hysteria2

bash
systemctl status hysteria-server
journalctl -u hysteria-server -f
systemctl restart hysteria-server
nano /etc/hysteria/config.yaml

MTProxy

bash
systemctl status telemt
journalctl -u telemt -f
systemctl reload telemt    # hot reload без разрыва соединений
systemctl restart telemt

Remnawave Panel

bash
cd /opt/remnawave
docker compose ps
docker compose logs -f remnawave
docker compose restart
🔧
Диагностика

Hysteria2 не запускается

bash
journalctl -u hysteria-server -n 50 --no-pager

Частые причины: порт занят, домен не резолвится, ACME не получил сертификат.

ACME не выдаёт сертификат

bash
# Открыть порт 80 и перезапустить
ufw allow 80/tcp
systemctl restart hysteria-server

# Проверить что порт 80 свободен
ss -tulpn | grep :80

# Проверить DNS (должны совпадать)
curl -4 ifconfig.me
dig +short cdn.example.com

Клиент не подключается

bash
systemctl status hysteria-server
ss -tulpn | grep PORT
ufw status | grep PORT

Сменить CA после установки

bash
nano /etc/hysteria/config.yaml
# Изменить строку: ca: zerossl
systemctl restart hysteria-server

DNS не обновился после переноса

bash
dig +short A cdn.example.com
ssh root@NEW_IP systemctl restart hysteria-server

Известные проблемы и решения

ПроблемаСимптомРешение
SPAWN_ERROR: xray Xray не стартует, порт занят Проверить что nginx.conf не содержит listen 443 ssl в selfsteal режиме. ss -tlnp | grep :443
inbounds:[] Нода получает пустой конфиг Панель → Nodes → редактировать → убедиться что inbound Steal (443) отмечен активным
Webhook ECONNREFUSED Remnawave не достучивается до hy-webhook Проверить WEBHOOK_URL=http://172.30.0.1:8766 (не 127.0.0.1). Проверить UFW: ufw status | grep 8766
hy-webhook слушает 127.0.0.1 Docker не видит webhook Добавить LISTEN_HOST=0.0.0.0 в /etc/hy-webhook.env, перезапустить
502 Bad Gateway на панели nginx не может достучаться до Remnawave Проверить что proxy_protocol соответствует источнику: unix-сокет требует proxy_protocol, прямой 443 — нет
Invalid subscription content Подписка не парсится клиентом Проверить логи: docker logs remnawave-subscription-page --tail=20. Убедиться что REMNAWAVE_PANEL_URL и REMNAWAVE_API_TOKEN заданы
🔄
Обновление

Вариант 1 — через меню рекомендуется

Если скрипт уже установлен и запущен:

menu
Главное меню → 5) Обновить скрипт

Скачивает архив с GitHub и обновляет все модули lib/*.sh.

Вариант 2 — полная переустановка с нуля

Если скрипта ещё нет или нужна чистая установка:

bash
mkdir -p /root/lib

for mod in common panel telemt hysteria migrate; do
    curl -fsSL "https://raw.githubusercontent.com/stump3/server-manager/main/lib/${mod}.sh" \
        -o "/root/lib/${mod}.sh"
done

curl -fsSL "https://raw.githubusercontent.com/stump3/server-manager/main/server-manager.sh" \
    -o /root/server-manager.sh && chmod +x /root/server-manager.sh

bash /root/server-manager.sh

Вариант 3 — обновить один модуль

Если нужно обновить только конкретный компонент:

bash
# Один модуль
curl -fsSL "https://raw.githubusercontent.com/stump3/server-manager/main/lib/panel.sh" \
    -o /root/lib/panel.sh

# Или все модули через архив
curl -fsSL https://github.com/stump3/server-manager/archive/refs/heads/main.tar.gz \
    | tar -xz --strip-components=2 -C /root/lib server-manager-main/lib
Скрипт управления rp хранится отдельно в /usr/local/bin/remnawave_panel и не обновляется автоматически.
После обновления модулей применить изменения:
Главное меню → Remnawave Panel → Управление → 12) Переустановить скрипт (rp)
📋
Требования

Система и порты

ПараметрЗначение
ОСUbuntu 20.04+ / Debian 11+
Праваroot
443 TCPXray (selfsteal) + nginx — должен быть свободен до установки
8443 UDPHysteria2
80 TCPcertbot — открывается на время SSL, потом закрывается
2222 TCPremnanode — только из Docker сети 172.30.0.0/16
ЗависимостиDocker, docker-compose, jq, certbot, openssl

RAM

⚠️
Минимум 2 GB RAM для полного стека. Рекомендуется 4 GB. При 2 GB сервисы в пике уходят в swap.
КомпонентRAM (факт)PeakТип
remnawave (NestJS)~395 MB~400 MBDocker
remnawave-db (Postgres)~50 MB + ~195 MB workers~260 MBDocker
remnanode (Xray rw-core)~88 MB~90 MBDocker
subscription-page~76 MB~80 MBDocker
telemt~18 MB~84 MBsystemd
hysteria2~17 MB~53 MBsystemd
nginx + redis~10 MB~15 MBDocker
hy-webhook~1 MB~12 MBsystemd
Итого~850 MB~1 GB
ℹ️
395 MB для remnawave — это норма для NestJS + BullMQ + TypeORM стека. Другие скрипты на базе remnawave/backend:2 (включая eGames) показывают те же цифры — конфиги идентичны.

Swap

Рекомендуется минимум 1 GB swap. При 2 GB RAM telemt и hysteria2 в пике уходят в swap (47–83 MB). Без достаточного swap OOM killer может убивать контейнеры.

bash — добавить 2GB swap
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
🔍
Качество кода — сравнение с eGames

Сравнение с eGames

АспектeGamesserver-manager
Обработка ошибок APIПодробные сообщения|| warn без деталей
ИдемпотентностьПовторный запуск ломаетIS_INSTALLED + компоненты
МодульностьМонолит 5000+ строк5 модулей, curl|bash
SSH рефакторингДублирование кодаask_ssh_target/helpers
Валидация UUIDПроверка перед APIЧастичная
API Client-Type headerX-Remnawave-Client-TypeX-Remnawave-Client-Type

Исправленные баги

🔴
panel_api() не была определена — функция вызывалась 9 раз в panel_install() но не существовала. Исправлено в v2.1.0.
⚠️
set -e в mgmt-скрипте — может прерывать при ненулевом exit code от grep/[ ]. eGames не использует set -e в mgmt-скрипте.
⚠️
Смешение jq и python3 для JSON в panel.sh — усложняет поддержку. Рекомендуется использовать jq везде.

Рекомендации для будущих версий

📝
Логирование
tee /var/log/server-manager-install.log — сохранять лог установки для диагностики
🔒
Атомарность
Флаг /opt/remnawave/.installed — предотвращать дубликаты при прерванной установке
Smoke test
После установки проверять что 443 отвечает и Xray принял соединение
🔄
Версии конфигов
CONFIG_VERSION в .env — предупреждать об устаревшем docker-compose при обновлении