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

328 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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]].
>
> **Осталось:** Alex добавил маршрут `mm → 8065` в CF-туннель (подтвердил 2026-09-26).
> Нужно: `SITEURL` → `https://mm.qentra.top` + приёмка входом в браузере.
---
## 🔴 Главная находка: официального 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-<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)`.
**Правильно — раздельно:**
```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` | `http://192.168.1.14:8065` (LAN, временно) | → поменять на `https://mm.qentra.top` после настройки туннеля |
| `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.
---
## Команды
```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 <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` | ⏳ **следующий шаг** |
| Приёмка входом в браузере | ⏳ **делает Alex** |
### Команды для следующего шага
```bash
# 1. Поменять SITEURL на внешний (после подтверждения, что mm.qentra.top открывается)
ssh kraken 'cd /home/kraken/mattermost && sed -i \
"s|MM_SERVICESETTINGS_SITEURL: .*|MM_SERVICESETTINGS_SITEURL: \"https://mm.qentra.top\"|" docker-compose.yml'
ssh kraken 'cd /home/kraken/mattermost && docker compose up -d mattermost'
# 2. Проверить, что MM отвечает через туннель (с Mac)
curl -s https://mm.qentra.top/api/v4/system/ping
# 3. Забрать пароль админа
ssh kraken 'sudo cat /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/mattermost/config/.adminpw'
```
⚠️ **Приёмка = вход в `https://mm.qentra.top` в браузере**, не «curl отвечает».
Иначе: `sed -i` в п.1 — временная мера; при пересборке compose из генератора SITEURL
перезапишется. Долгосрочно — поменять значение в генераторе скрипта.
## Связанные заметки
- [[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 для Балды