Files
obsidian-vault/personal/projects/balda/mattermost-kraken-deploy.md
T

30 KiB
Executable File
Raw Blame History

[!WARNING] Историческая схема Balda Не использовать пути, контейнеры, provider или MCP transport из этой заметки для текущего запуска. Канонический текущий runbook: personal/projects/balda/setup. Актуально: balda-mm-mac, /Users/admin/Docker/balda-mm-mac/config/config.yaml, Mattermost, Codex ACP, Obsidian type: http → http://host.docker.internal:3101/mcp.

Mattermost на Kraken — деплой (arm64)

Создано 2026-09-26. Обновлено 2026-09-26. Статус: ✅ СЕРВЕР РАБОТАЕТ. health=healthy restarts=0, API {"status":"OK"} с Kraken и с Mac по LAN, админ создан, регистрация закрыта (проверено живым запросом → HTTP 501). Часть проекта personal/projects/balda/balda-mm-delegation-plan.

Сделано (2026-09-26, позже): SITEURL → https://mm.qentra.top, MM перезапущен → health=healthy restarts=0. Ping через туннель с Mac: {"status":"OK"}. mmctl config get ServiceSettings.SiteURL → https://mm.qentra.top.

Осталось: приёмка входом в браузере (делает Alex).

Обновлено 2026-09-29: добавлен третий бот — whale (Кит) для транспорта hermes-whale. Канал whale. Разбор — personal/tech/hermes-whale-mattermost-transport.md.

Объект ID
Бот whale k4i3uihzp3gdxj5gx3en3tbeso
Канал whale fnm6auyc8trz5njxrfofebkbfr
PAT бота len=26, проверен users/me → username: whale

Всего ботов на инстансе — 3: system-bot, balda, whale. Каналов в команде qentra — 4: town-square, off-topic, balda-test, whale.


🔴 Главная находка: официального arm64-образа Mattermost НЕТ

mattermost/mattermost-team-edition — только linux/amd64. Проверено docker manifest inspect по тегам latest, release-12, release-10, 11.11.1, 10.11.24, 9.11, release-8 — везде architecture: amd64. То же у mattermost-enterprise-edition и mattermost-preview.

Официальная позиция Mattermost (KB 53131125053204, на 2026-09-01): ARM64 «Not officially supported for production, no roadmap commitment». Публикуются некоторые arm64-сборки, но production не поддерживается; официально поддержан только amd64.

⚠️ linuxserver/mattermost НЕ существует — pull access denied. На Docker Hub его нет. Не искать его снова (я потратил на это время).

Решение: собираем образ сами из ОФИЦИАЛЬНОГО бинаря

Mattermost публикует arm64-бинарники на release-сервере — просто не пакует их в образы. Поэтому образ собирается из официального tarball. Это production-путь: свой Dockerfile + upstream-бинарь, без сторонних community-образов.

🔴 СБОРКА ИДЁТ НА MAC, НЕ НА KRAKEN (решение Alex, 2026-09-26 — исправление моей ошибки).

Я сначала собирал на Kraken. Alex указал на два дефекта, оба реальные:

  1. Mac — тот же linux/arm64 (Apple Silicon), Docker нативный, без эмуляции. Замер: сборка на Mac — 33 секунды. На Kraken тот же Dockerfile падал после 346 с на одном только apt-слое.
  2. 🔴 Buildkit-кэш на Kraken живёт на SD-карте, а не на HDD. data-root перенесён на USB-HDD, но buildkit cache ему не подчиняется. Это записано в openmediavault-rpi5.md §«Build cache (buildkit) — живёт на SD!». Сборка льёт apt-get, curl tarball 157 МБ и слои именно на 14-гигабайтную SD — ровно то, от чего разгружали Kraken в инциденте 2026-06-24 (SD под 99%, краш). Урок: тяжёлые сборки образов для Kraken — НИКОГДА на Kraken.

Перенос образа на Kraken:

# на Mac
docker build --platform linux/arm64 -t mattermost-team-arm64:11.11.1 .
docker save ... | zstd -3 -T0 -o mm-arm64.tar.zst
# передача (zstd -dc на стороне Mac, распаковка на хосте)
zstd -dc mm-arm64.tar.zst | ssh kraken 'docker load'

⚠️ Питфолл: zstd -dc file.zst | ssh kraken 'zstd -d | docker load' не работает — zstd на Kraken (v1.5.4) отказывается читать фрейм, записанный macOS-zstd (v1.5.7): unsupported format. Распаковывать на стороне Mac, на хост отдавать уже docker load.

После переезда: docker builder prune -af на Kraken вернул SD к 36% (кэш 297 МБ удалён).

Проверка доступности tarball (HTTP 200, content-length: 157689127):

https://releases.mattermost.com/11.11.1/mattermost-team-11.11.1-linux-arm64.tar.gz

Работает и mattermost-<ver>-linux-arm64.tar.gz (без team). Префикс mattermost-team- — Team Edition, это то, что нужно. Собранная версия подтверждена: Version: 11.11.1, Build Enterprise Ready: false.

Альтернативы, которые НЕ выбраны:

Вариант Почему нет
cyberman443/mattermost-arm64 сторонний, 5 мес. давности, непроверенная сборка
ghcr.io/antonsatt/mattermost-arm64 сторонний; CI собирает weekly, но доверия меньше, чем своему Dockerfile
mmtrt/…, ctlove0523/…, bitnami/mattermost не существуют / нет arm64

Референс структуры Dockerfile: github.com/AntonSatt/mattermost-arm64 (там ubuntu:noble + официальный tarball; нюанс — в их ARG по умолчанию enterprise-путь без -team, у нас явно team).


Файлы и пути

Что Путь
Каталог compose /home/kraken/mattermost/
docker-compose.yml /home/kraken/mattermost/docker-compose.yml (0600)
.env (пароль postgres) /home/kraken/mattermost/.env (0600, 3 строки)
Пароль админа MM …/docker/mattermost/config/.adminpw (0600, uid 2000)
Бэкап compose /home/kraken/mattermost/backups/docker-compose.yml.bak-<stamp>
Данные и тома /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/mattermost/{postgres,data,logs,plugins,config,client-plugins,bleve-indexes}
Dockerfile сборки (Mac) /tmp/mm-build/Dockerfile
Образ mattermost-team-arm64:11.11.1 (+ :latest)
Скрипты (Mac) /tmp/mm-deploy/{build-on-mac,deploy-mattermost,ship-to-kraken,reload-stack,fix-ownership,fix-ownership2,provision-admin,harden-registration,align-config,wait-health,final-check}.sh, patch_compose.py

⚠️ Скрипты в /tmp — не переживут reboot Mac. Всё существенное продублировано в этом доке; при необходимости воспроизводить по разделам ниже.

⚠️ Путь 49e8f586… — симлинк на реальный 6194539b…. Все сервисы используют старый путь.

Владельцы каталогов — КРИТИЧНО (uid-конфликт, дважды укусил)

Каталог Владелец Почему
mattermost/postgres 70:70 uid postgres-образа postgres:16-alpine
всё остальное в mattermost/ 2000:2000 uid юзера в нашем образе (useradd --uid 2000)

Как сломалось дважды (один корень, два симптома):

  1. Каталоги создал kraken (uid 1000), контейнер работает под 2000 → could not create config file: open /mattermost/config/config.json: permission denied.
  2. Я исправил слишком широко: chown -R 2000:2000 задел и postgres/ → postgres (uid 70) потерял доступ → pq: could not open file "global/pg_filenode.map": Permission denied (42501).

Правильно — раздельно:

ROOT=/srv/dev-disk-by-uuid-49e8f586-.../docker/mattermost
sudo -n chown -R 70:70   $ROOT/postgres
for d in config data logs plugins client-plugins bleve-indexes; do sudo -n chown -R 2000:2000 $ROOT/$d; done
sudo -n chown 2000:2000 $ROOT

📌 Это тот же класс дефекта, что crash-loop hermes-kraken (uid bind-mount). При любом новом сервисе на Kraken — сразу приводить владельца тома к uid из образа.

Питфолл: chown -R на медленном USB-HDD может пережить разрыв SSH. Запускать через sudo -n systemd-run --unit=mm-chown --collect --property=Type=oneshot chown -R …, потом опрашивать systemctl is-active mm-chown.

.env формат (3 строки, значения не в доке)

PGPW=<24 байта, openssl rand -hex 24>
PGUSR=mmuser
PGDBNAME=mattermost

Генерируется на хосте при первом запуске скрипта; повторный запуск не перезаписывает (if [ -s .env ]; then exists_kept).

docker-compose.yml — ключевые параметры

Ключ Значение Почему
образ MM mattermost-team-arm64:11.11.1 вместо официального amd64
порт 8065:8065 был свободен (проверено ss -tln)
postgres postgres:16-alpine arm64 есть
MM_SQLSETTINGS_DRIVERNAME postgres
MM_SQLSETTINGS_DATASOURCE postgres://mmuser:***@postgres:5432/mattermost?sslmode=disable&connect_timeout=10 пароль подставляется в момент генерации compose из .env
MM_SERVICESETTINGS_SITEURL ✅ https://mm.qentra.top (обновлено 2026-09-26; было http://192.168.1.14:8065) MM отдаёт ссылки и WS-URL по этому хосту; LAN-значение ломало бы WebSocket через туннель
MM_SERVICESETTINGS_ENABLELOCALMODE true нужен для healthcheck mmctl --local
MM_TEAMSETTINGS_ENABLEOPENSERVER "false" 🔴 закрытая регистрация
MM_TEAMSETTINGS_ENABLEUSERCREATION "false" 🔴 закрытая регистрация
MM_FILESETTINGS_DIRECTORY /mattermost/data
healthcheck MM curl -fsS http://localhost:8065/api/v4/system/ping curl есть в ubuntu:noble-базе
healthcheck PG pg_isready -U mmuser -d mattermost
restart unless-stopped
depends_on postgres service_healthy MM не стартует раньше БД
сеть bridge mm-net (не host) MM не требует host-сети, в отличие от cloudflared

Питфолл: MM_SQLSETTINGS_DATASOURCE — это URL, Mattermost читает env литерально, интерполяция ${PGPW} внутри значения не работает. Поэтому DSN собирается на хосте через printf при записи compose, а не через env_file.

🔴 Закрытая регистрация — состояние и как проверено

Требование Alex (2026-09-26): регистрация в MM должна быть закрытая.

Действующие ключи в Mattermost 11.x (проверено mmctl config get TeamSettings):

Ключ Статус
TeamSettings.EnableOpenServer ✅ существует
TeamSettings.EnableUserCreation ✅ существует
TeamSettings.EnableOpenTeams ❌ удалён в 11.x (selected path object is not valid)
TeamSettings.RestrictTeamInvitations ❌ удалён в 11.x (invalid key)
TeamSettings.RestrictCreationToDomains ✅ существует (пусто = без доменных ограничений)

⚠️ Не использовать EnableOpenTeams / RestrictTeamInvitations — их нет в 11.x. Значения MM_TEAMSETTINGS_ENABLEOPENTEAMS / _RESTRICTTEAMINVITATIONS оставлены в compose как безвредные (Mattermost их игнорирует), но реально работают только два Enable*.

🔴 Env-переменные перебивают config.json. mmctl config set пишет в config.json, но env имеет приоритет. Поэтому config.json показывал EnableUserCreation: true, а рантайм — false. Расхождение устранено: config.json приведён к false вручную, источник истины — env в compose (переживает пересоздание контейнера).

Живая проверка (не по логам):

ssh kraken 'curl -s -X POST http://localhost:8065/api/v4/users -H "Content-Type: application/json" \
  -d "{\"email\":\"probe@example.com\",\"username\":\"probeuser\",\"password\":\"ProbePass12345!\"}"'
# → {"id":"api.user.create_user.signup_email_disabled.app_error", ... "status_code":501}

HTTP 501 = самописная регистрация отбита. Это и есть доказательство, что закрыто.

Админ

# создать (идемпотентно; пароль генерируется на хосте, не логируется)
docker exec mattermost sh -c 'PW=$(head -c 24 /dev/urandom | base64 | tr -d "=+/" | head -c 28); \
  mmctl user create --local --email admin@qentra.top --username admin --password "$PW" --system-admin'
# пароль: …/docker/mattermost/config/.adminpw (0600)

Пользователь: admin / admin@qentra.top. Токен для Балды — создавать не надо, даст Alex. ⚠️ Отменено 2026-09-26 (вечер): по команде Alex бот создан автоматически, см. §«Бот balda».


Боты на MM (реестр)

Бот user_id Владелец Назначение
balda edxrgupoxiyig5gzmr79tjypbme admin Фаза 3 — personal/projects/balda/balda-kraken-mm-deploy
system-bot ackaowjyjbrutjgknr16d5mfry admin системный, MM
whale (Кит) k4i3uihzp3gdxj5gx3en3tbeso whale транспорт hermes-whale, 2026-09-29

Бот whale — создан 2026-09-29

Для транспорта hermes-whale. Полный разбор — personal/tech/hermes-whale-mattermost-transport.md.

# создать юзера → сконвертировать в бота (ШЕСТЬ флагов обязательны, --owner НЕ существует)
mmctl --local user create --email whale@localhost --username whale --password '<rand>'
mmctl --local user convert whale --bot --email whale@localhost \
  --firstname Whale --lastname Kit --locale en --system-admin
# → whale converted to bot.

🔴 mmctl user convert НЕ выдаёт токен. Токен выпускается отдельно и показывается ровно один раз: mmctl --local token generate whale whale-hermes.

Канал whale = fnm6auyc8trz5njxrfofebkbfr (создан channel create --team qentra). Бот в канале ✅, в команде — ⚠️ не подтверждено (team users list в mmctl не существует).


Бот balda (создан 2026-09-26, вечер)

Нужен для Фазы 3 — personal/projects/balda/balda-kraken-mm-deploy.

Что Значение
username balda
user_id edxrgupoxiyg5gzmr79tjypbme
email balda@localhost
owner admin (cbwsorzwoigbtrdnteugnx4aqo)
Токен /home/kraken/balda/secrets/mm-bot-token (0600 на хосте)
token id ejquewpbq3yq7fjg57crbdp9qw → /home/kraken/balda/secrets/mm-bot-token-id
Проверка GET /api/v4/users/me → username=balda is_bot=true ✅

🔴 EnableBotAccountCreation — был false, включать только через mmctl

POST /api/v4/bots отвечал HTTP 403 api.bot.create_disabled. Значение живёт в ServiceSettings.EnableBotAccountCreation.

REST PUT /api/v4/config здесь НЕ работает: возвращает 200, но значение не меняется — env-переменные MM перебивают config.json (тот же механизм, что и у EnableUserCreation, см. §«Закрытая регистрация»).

ssh kraken 'docker exec mattermost mmctl --local config set ServiceSettings.EnableBotAccountCreation true'
# → Value changed successfully / "true"

🔴 Mattermost отдаёт значение токена РОВНО ОДИН РАЗ

POST /api/v4/users/{id}/tokens → в ответе есть token. Повторный GET /api/v4/users/{id}/tokens возвращает только id, description, is_active, expires_at, user_id — поля token там нет. Токен восстановить нельзя, только создать новый.

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

curl -s -X POST $MM_API/users/$BOT_ID/tokens -H "$AUTH $SESS" \
  -H 'Content-Type: application/json' -d '{"description":"balda-orchestrator"}' \
  > /tmp/token-create.json
/usr/bin/jq -r '.token' /tmp/token-create.json > /home/kraken/balda/secrets/mm-bot-token
rm -f /tmp/token-create.json
chmod 600 /home/kraken/balda/secrets/mm-bot-token

🔴 Сессионный токен админа — в ЗАГОЛОВКЕ ответа, не в теле

HDRS=$(curl -s -D- -o /dev/null -X POST $MM_API/users/login \
  -H 'Content-Type: application/json' -d '{"login_id":"admin","password":"$PW"}')
SESS=$(printf '%s' "$HDRS" | tr -d '\r' | awk 'tolower($1)=="token:"{print $2}')

В JSON-теле поля token нет — легко принять за сломанный логин.

⚠️ Скрипты провижининга писать с оглядкой на три питфолла редакции секретов Hermes (переменная TOKEN, литерал заголовка авторизации, $PW во вложенном heredoc) — personal/tech/hermes-secret-redaction-bypass §«Питфоллы… MM-бота».


Команды

# Сборка образа (на Kraken, ~минуты; запускать в background)
bash /tmp/mm-deploy/build-mattermost-arm64.sh

# Деплой (идемпотентный)
bash /tmp/mm-deploy/deploy-mattermost.sh

# Проверка
ssh kraken "curl -s http://localhost:8065/api/v4/system/ping"
ssh kraken "docker ps --format '{{.Names}}\t{{.Status}}' | grep -E 'mattermost|mm-postgres'"

Питфоллы, найденные на месте

  1. 🔴 Mattermost 11.x: CMD ["mattermost"] НЕ работает. Бинарь печатает help и выходит с exit 1. Нужна подкоманда: CMD ["mattermost", "server"]. Симптом в контейнере: bare_exit=1, бесконечный цикл starting/unhealthy, логи — только список подкоманд (db, export, jobserver, server, version, …). Проверка: docker inspect mattermost --format '{{.Config.Cmd}}' → должно быть [mattermost server].
  2. 🔴 tar --strip-components=1 <member> не водворит дерево под нужный корень. В Dockerfile tar -xzf mm.tar.gz -C / --strip-components=1 mattermost распаковывает bin/, templates/, NOTICE.txt в корень /, а не в /mattermost. Следствие: test -x /mattermost/bin/mattermost падает (exit 1), сборка рушится без внятной ошибки. Правильно: распаковать в staging-каталог, потом cp -a /staging/mattermost/. /mattermost/.
  3. Права файлов в tarball — rwxrwxrwx. После распаковки явно выставить chmod 0755 /mattermost/bin/{mattermost,mmctl} и chmod -R a+rX /mattermost.
  4. /srv/.../docker/ — root:root 755 → kraken НЕ может создать подкаталог. Нужен sudo -n mkdir -p. sudo -n true на Kraken работает без пароля (проверено, exit 0).
  5. sudo через SSH требует < /dev/null — иначе Connection closed by remote host. (Подтверждено, питфолл из kraken-access актуален.)
  6. Нет ifconfig на Kraken — использовать ip addr / ss.
  7. Один ssh-heredoc вместо десятка round-trip'ов — ssh kraken 'bash -s' <<'REMOTE'. Быстрее и надёжнее, чем отдельные вызовы.
  8. 🔴 Вложенные кавычки в heredoc через ssh ломаются молча. Мой патч compose через ssh kraken bash -s <<REMOTE … python3 - <<'PY' … PY … REMOTE не применился, не выдав ошибки (шаг «== 2. patch ==» отработал, PATCH_OK не напечатался, файл не изменился). Правильно: писать скрипт файлом на Mac, затем ssh kraken 'cat > /path/x.py' < x.py и ssh kraken 'python3 /path/x.py'. Это же записано в доке Kraken как питфолл.
  9. docker manifest inspect на приватном/несуществующем образе даёт unauthorized: authentication required, а не not found — не путать с «образ существует».
  10. Долгие команды в ssh вешают вызов. docker run --rm … server --help (запуск нового контейнера) ушёл в таймаут 400 с. Проверять CLI внутри уже запущенного контейнера через docker exec, а не через docker run.

Cloudflare Tunnel — что настроить (делает Alex)

Туннель kraken уже существует, remote-managed по TUNNEL_TOKEN, network_mode: host.

Zero Trust → Networks → Tunnels → kraken → Public Hostnames → Add:

Поле Значение
Subdomain mm
Domain qentra.top
Path (пусто)
Service Type HTTP
URL localhost:8065

⚠️ httpHostHeader НЕ ставить — в отличие от kraken.qentra.top, где он обязателен.

⚠️ Без Access-Applications хостнейм mm.qentra.top публично открыт в интернет. В Team Edition регистрация по умолчанию open — закрывать политикой Access или выключать самостоятельную регистрацию в System Console.

После добавления хостнейма — поменять MM_SERVICESETTINGS_SITEURL на https://mm.qentra.top и перезапустить MM. Иначе MM отдаёт ссылки на 192.168.1.14, и WebSocket-клиент может не подключиться.

Приёмка = вход в https://mm.qentra.top в браузере, не «curl отвечает».


Состояние (2026-09-26, обновлено после приёмки)

Шаг Статус
Каталоги на HDD ✅ созданы, владельцы исправлены (см. §Владельцы)
.env с паролем ✅ создан, 0600, значения не логировались
docker-compose.yml ✅ записан + пропатчен (arm64-образ, закрытая регистрация)
Dockerfile + arm64-образ ✅ собран на Mac за 33 с, перенесён docker load
docker compose up -d ✅ оба контейнера Up
Health ✅ health=healthy restarts=0 status=running
/api/v4/system/ping с Kraken ✅ {"status":"OK"}
Ping с Mac по LAN 192.168.1.14:8065 ✅ {"status":"OK"}
Порт ✅ 0.0.0.0:8065 (cloudflared видит)
Версия ✅ 11.11.1 Team Edition, arm64, Enterprise Ready: false
Миграции БД ✅ завершены, websocket-хабы подняты
Админ admin@qentra.top ✅ создан, пароль в .adminpw
Регистрация закрыта ✅ HTTP 501 на самописную регистрацию (живая проверка)
config.json = рантайм ✅ приведены к согласию (EnableUserCreation/OpeNserver: false)
Cloudflare mm → 8065 ✅ добавил Alex 2026-09-26 («up»)
SITEURL → https://mm.qentra.top ✅ сделано 2026-09-26 (бэкап → скрипт → compose up -d → проверка)
Ping через туннель https://mm.qentra.top/api/v4/system/ping ✅ {"status":"OK"}
mmctl config get ServiceSettings.SiteURL ✅ "https://mm.qentra.top"
Приёмка входом в браузере ⏳ делает Alex

Как менялся SITEURL (2026-09-26, выполнено)

Порядок по правилу «коммит ДО → правка → заливка → проверка»:

  1. Бэкап compose на хосте: cp docker-compose.yml backups/docker-compose.yml.bak-$(date +%Y%m%d-%H%M%S) → /home/kraken/mattermost/backups/docker-compose.yml.bak-20260926-103937.
  2. Правка скриптом, не sed. /tmp/mm-deploy/set-siteurl.py (локально на Mac) → ssh kraken 'cat > /tmp/set-siteurl.py' < set-siteurl.py → ssh kraken 'python3 /tmp/set-siteurl.py'. Скрипт идемпотентен (NOOP при совпадении), пишет во временный файл и делает атомарный replace. Вывод: OK: MM_SERVICESETTINGS_SITEURL: "http://192.168.1.14:8065" -> MM_SERVICESETTINGS_SITEURL: "https://mm.qentra.top".
  3. Перезапуск: ssh kraken 'cd /home/kraken/mattermost && docker compose up -d mattermost' → mattermost Recreated → Started, mm-postgres Healthy.
  4. Ожидание health (docker inspect mattermost --format '{{.State.Health.Status}} restarts={{.RestartCount}}'): starting ×8 итераций по 5 с → healthy restarts=0 на 9-й (~45 с).
  5. Проверка с Mac: curl -s https://mm.qentra.top/api/v4/system/ping → {"status":"OK",...}; docker exec mattermost mmctl --local config get ServiceSettings.SiteURL → "https://mm.qentra.top".

⚠️ sed -i из старой инструкции ниже не использовать — он оставлен как история. Правка compose — только скриптом с бэкапом. ⚠️ При пересборке compose из генератора SITEURL перезапишется на LAN-значение. Долгосрочно — менять значение в генераторе, а не в файле.

⚠️ Приёмка = вход в https://mm.qentra.top в браузере, не «curl отвечает». Приёмка SITEURL и техническая проверка через туннель — сделаны (см. таблицу выше).

Осталось одно: Alex открывает https://mm.qentra.top в браузере и логинится admin.

Пароль админа получен 2026-09-26 и передан Alex'ом в чат (значение в доке не хранится — только путь к файлу). Если понадобится снова:

# Забрать пароль админа
ssh kraken 'sudo cat /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/mattermost/config/.adminpw'
# Вывод вида: PW=<28 символов>   ← в контейнере файл лежит без префикса

⚠️ Файл принадлежит uid 2000 и лежит в config/ — читать только через sudo -n, и только при наличии < /dev/null в ssh-команде (иначе Connection closed by remote host).

Проверка, что всё живо (одна команда)

ssh kraken "docker inspect mattermost --format '{{.State.Health.Status}} restarts={{.RestartCount}}' && curl -s http://localhost:8065/api/v4/system/ping" < /dev/null
curl -s -m 20 https://mm.qentra.top/api/v4/system/ping     # с Mac, через туннель

Ожидаемо: healthy restarts=0 + {"status":"OK",...} в обоих случаях.

Связанные заметки