28 KiB
Executable File
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 указал на два дефекта, оба реальные:
- Mac — тот же
linux/arm64(Apple Silicon), Docker нативный, без эмуляции. Замер: сборка на Mac — 33 секунды. На Kraken тот же Dockerfile падал после 346 с на одном только apt-слое. - 🔴 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) |
Как сломалось дважды (один корень, два симптома):
- Каталоги создал
kraken(uid 1000), контейнер работает под 2000 →could not create config file: open /mattermost/config/config.json: permission denied. - Я исправил слишком широко:
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».
Бот balda (создан 2026-09-26, вечер)
Нужен для Фазы 3 — personal/projects/balda/balda-kraken-mm-deploy.
| Что | Значение |
|---|---|
| username | balda |
| user_id | edxrgupoxiyg5gzmr79tjypbme |
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'"
Питфоллы, найденные на месте
- 🔴 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]. - 🔴
tar --strip-components=1 <member>не водворит дерево под нужный корень. В Dockerfiletar -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/. - Права файлов в tarball —
rwxrwxrwx. После распаковки явно выставитьchmod 0755 /mattermost/bin/{mattermost,mmctl}иchmod -R a+rX /mattermost. /srv/.../docker/—root:root 755→krakenНЕ может создать подкаталог. Нуженsudo -n mkdir -p.sudo -n trueна Kraken работает без пароля (проверено, exit 0).sudoчерез SSH требует< /dev/null— иначеConnection closed by remote host. (Подтверждено, питфолл изkraken-accessактуален.)- Нет
ifconfigна Kraken — использоватьip addr/ss. - Один ssh-heredoc вместо десятка round-trip'ов —
ssh kraken 'bash -s' <<'REMOTE'. Быстрее и надёжнее, чем отдельные вызовы. - 🔴 Вложенные кавычки в 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 как питфолл. docker manifest inspectна приватном/несуществующем образе даётunauthorized: authentication required, а неnot found— не путать с «образ существует».- Долгие команды в
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, выполнено)
Порядок по правилу «коммит ДО → правка → заливка → проверка»:
- Бэкап 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. - Правка скриптом, не 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". - Перезапуск:
ssh kraken 'cd /home/kraken/mattermost && docker compose up -d mattermost'→mattermost Recreated → Started,mm-postgres Healthy. - Ожидание health (
docker inspect mattermost --format '{{.State.Health.Status}} restarts={{.RestartCount}}'):starting×8 итераций по 5 с →healthy restarts=0на 9-й (~45 с). - Проверка с 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",...} в обоих случаях.
Связанные заметки
- 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 для Балды