# 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). --- ## 🔴 Главная находка: официального 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: ```bash # на 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--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-` | | Данные и тома | `/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)`. **Правильно — раздельно:** ```bash 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 (переживает пересоздание контейнера). **Живая проверка (не по логам):** ```bash 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 = самописная регистрация отбита. Это и есть доказательство, что закрыто. ### Админ ```bash # создать (идемпотентно; пароль генерируется на хосте, не логируется) 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`». --- ## Бот `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`, см. §«Закрытая регистрация»). ```bash 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 байт. Правильный порядок — **перехватить значение в момент создания** и сразу записать в файл: ```bash 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 ``` ### 🔴 Сессионный токен админа — в ЗАГОЛОВКЕ ответа, не в теле ```bash 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-бота». --- ## Команды ```bash # Сборка образа (на 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 ` не водворит дерево под нужный корень.** В 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 < /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'ом в чат (значение в доке не хранится — только путь к файлу). Если понадобится снова: ```bash # Забрать пароль админа 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`). ### Проверка, что всё живо (одна команда) ```bash 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",...}` в обоих случаях. ## Связанные заметки - [[personal/projects/balda/balda-mm-delegation-plan]] — общий план MM + делегирования - [[family/how-to/kraken-access]] — доступ к Kraken, cloudflared, порты - [[family/how-to/openmediavault-rpi5]] — структура Docker на Kraken - [[personal/tech/docker-data-root-mount-race]] — почему том на HDD, а не на SD - [[personal/projects/balda/balda-mattermost-connector]] — коннектор MM для Балды