11 KiB
mappa → VDS deploy runbook
Развёртывание mappa (единый meta-сервис цеха: HTTP-ядро + MCP-адаптер, Postgres)
на ../entities/vds-kzntsv (89.253.255.94) рядом с общей БД (решения 1/17/18 спеки
concepts/mappa). Первый деплой — 2026-08-23 (#1012, закрыта).
Разделение труда (директива 2026-08-23): деплой/редеплой = АДМИН (таска + письмо); прод-импорт, verify счётчиков, read-only+фолбэк, ntfy-алерт, артефакты, MCP-регистрация = mappa (#985). Не делать чужую часть.
Артефакты
- Код:
OpeItcLoc03/mappa@ master. Сборка TS:npm ci && npm run build→dist/. - Образ:
registry.kzntsv.site/mappa:<version>— тег = version из package.json, GIT_SHA передаётся--build-arg GIT_SHA=<sha>(Dockerfile в корне репо, multi-stage node:22-alpine; обязательный COPYsrc/lib/schema.sql→dist/src/lib/schema.sql— tsc не копирует .sql, без него migrate() падает «schema.sql not found»). Прод на 2026-08-28 = v0.29.0 (GIT_SHA 4e013bd). Deploy-паттерн: релизная веткаdeploy/vX.Y.Zиз батч-tip → бамп package.json → merge в master. - Стек Portainer:
mappa(Id 27, endpoint 1). Source-of-truth compose:admin/host-stacks/vds-kzntsv/mappa.compose.yml. - БД: база
mappa+ rolemappaна общемpostgres:16VDS (owner = mappa). Креды:pass show mappa/full-env(MAPPA_DB_PASS,DATABASE_URLdocker-сетевой:postgres://mappa:…@postgres:5432/mappa; внешний hostpostgres.vds.kzntsv.site:5432— TLS-verify-skip). - Endpoint:
https://mappa.vds.kzntsv.site(traefik websecure, letsEncrypt, service-port 8731).
Реденплой (обновление образа) — быстрый путь
# 1. Собрать и запушить новый тег образа (с воркстейшена, Docker Desktop)
cd ~/projects/mappa
docker build -t registry.kzntsv.site/mappa:<tag> .
docker push registry.kzntsv.site/mappa:<tag>
# 2. НА VDS: docker pull ДО restart стека (иначе застрянет на старом digest!)
ssh vds.kzntsv.site 'docker pull registry.kzntsv.site/mappa:<tag>'
# 3. Поменять image: в host-stacks/vds-kzntsv/mappa.compose.yml И в Portainer stack 27
# (обе копии синхронизировать — иначе следующий redeploy из гита откатит тег)
# 4. Обновить стек через Portainer API (НЕ ad-hoc docker compose up!): PUT /api/stacks/27?endpointId=1
# с {stackFileContent (новый тег), env 3/3, pullImage:true} — X-API-Key: pass vds-kzntsv/full-env PORTAINER_API_KEY.
# PUT без env сбрасывает env-массив (gotcha 4b) — env обязателен. pullImage:true тянет новый digest сам.
# Рестарт-only (POST /restart) НЕ меняет тег образа — только для рестарта без смены образа.
# 5. Verify
curl -ksS https://mappa.vds.kzntsv.site/health # {"ok":true,"service":"mappa"}
curl -ksS https://mappa.vds.kzntsv.site/admin/status # entities/by_type/projects
curl -ksS https://mappa.vds.kzntsv.site/graph/stats # nodes/edges/components
ssh vds.kzntsv.site 'docker ps --filter name=mappa --format "{{.Status}}"'
Обновление env (пароль БД и т.п.)
Через Portainer → Stack 27 → Edit (env array), НЕ файлом на VDS (Portainer string-mode не
материализует .env; env передаётся API-массивом — gotcha 1 portainer-stack-management-vds).
Env 3/3 (все из pass mappa/full-env): MAPPA_DB_PASS (подставляется в DATABASE_URL compose),
MAPPA_GITEA_TOKEN, MAPPA_API_TOKEN.
Первичный деплой с нуля (cold start)
- БД: на VDS
docker exec postgres psql -U postgres:CREATE ROLE mappa LOGIN PASSWORD '<pw>'; CREATE DATABASE mappa OWNER mappa;Пароль →pass insert -m mappa/full-env. - Образ: собрать (см. выше),
docker push. - Compose:
admin/host-stacks/vds-kzntsv/mappa.compose.yml→ Portainer create (standalone string-mode, endpointId=1, env[{"name":"MAPPA_DB_PASS","value":…}]). - Проверка миграций: при старте migrate() сам создаёт таблицы (6 шт: projects/counters/
entities/edges/leases/logs, owner=mappa) —
docker logs mappa+\dtв БД. - Пинг mappa: письмо в
mappa/.agents/inbox/(event: mappa-deployed) — импорт и остальное за ними.
Мониторинг живости (монитор → ntfy)
Остаток #985 (решение 15/18: алерт при падении сервиса) — реализован #1013 (2026-08-23).
- Скрипт:
/root/mappa-ntfy-monitor/monitor.py(python3 stdlib; источник:admin/scripts/mappa-ntfy-monitor/monitor.py). - Проверка: GET
https://mappa.vds.kzntsv.site/health— HTTP 200 + JSON{"ok":true,"service":"mappa"}(таймаут 15s). - Алерт: ntfy топик
mappa-alerts(basic-auth vitya, admin-role → rw все топики), после 3 подряд фейлов (~15 мин), раз за эпизод (dedupe); recovery-сообщение при возврате. - Крон:
/etc/cron.d/mappa-ntfy-monitor—*/5 * * * *(root, лог/var/log/mappa-ntfy-monitor.log). - Конфиг:
/root/.mappa-ntfy-monitor.env(600; NTFY_* изpass vds-kzntsv/full-env). State:/root/.mappa-ntfy-monitor.state. - ⚠️ Монитор НЕ трогает стек 27 — только наблюдение; редеплой/рестарт — отдельной задачей.
Gotchas (из первых деплоев)
- schema.sql в dist — tsc компилирует только .ts;
migrate()ищет.sqlрядом сdist/src/lib/. Без COPY в Dockerfile → контейнер в restart-loop («schema.sql not found»). - docker pull до restart — Portainer start/stop НЕ тянет новый digest сам по себе; перезалитый тег без pull на VDS = старый образ (ловил 2× за сессию 2026-08-23; начиная с v0.23.0 tag-смена идёт через PUT + pullImage:true — pull не нужен отдельно, но безвреден).
- Не ad-hoc compose — все стеки VDS через Portainer (VDS ops rule).
docker rm -fвручную снёс контейнер — Portainer recreate поднял, но это не путь. - mem_limit 512m — канон app-стеков, не убирать (cgroup-лимит vs вся память хоста).
4b. PUT stack сбрасывает env! — при обновлении compose через
PUT /api/stacks/:idPortainer обнуляет env-массив стека (Env: null), если не передатьenvв payload. Симптом: контейнер в restart-loop сSASL: client password must be a string(подстановка${MAPPA_DB_PASS}падает). Фикс: повторный PUT с{stackFileContent, env:[{name,value}×3]}(значения изpass mappa/full-env). Поймано на деплое v0.13.1 (2026-08-24). - Внешний DATABASE_URL — контейнерный
@postgres:5432валиден только внутри docker-сети; для импорта с воркстейшена —postgres.vds.kzntsv.site:5432(+ TLS-verify-skip, self-signed). - Закрытие деплоя — всегда пара «таска + письмо mappa» (канон
mappa-deploy-workflow). - E2 backfill с воркстейшена (task:1274, 2026-08-26) — скрипт
scripts/backfill-frontmatter-tags.tsв репо (tsx). Нужен ВНЕШНИЙ DATABASE_URL (не docker-сетевой):postgres://mappa:<MAPPA_DB_PASS>@postgres.vds.kzntsv.site:5432/mappa?sslmode=require+NODE_TLS_REJECT_UNAUTHORIZED=0(self-signed, gotcha 5). dry-run →--apply. Идемпотентен (ON CONFLICT DO NOTHING); повторный прогон добавляет только новые страницы. Первый прогон: 2539 тегов / 6555 рёбер; проверку «секреты не стали тегами» — скан имён на sk-/AKIA/JWT перед apply (имена password/token — теги-темы, не значения). - Одноразовый ре-индекс tsvec при создании конфига токенизатора (v0.28.0, 2026-08-28) —
миграция создаёт PG-конфиг
mappa(гейт newly_created) и перестраивает tsvec ВСЕХ сущностей (~13.5k коммитов). Старт после деплоя дольше обычного; health может отвечать 404/не стартовать до ~1 мин — это ожидаемо, не паника. Проверятьdocker ps(health: starting) + повторять /health. - attachment upload: boundary регистрозависим, content-type lowercases (v0.29.0, 2026-08-28) —
сервер приводит content-type к lowercase (
ct.toLowerCase()), а boundary в multipart — регистрозависим (RFC 2046). Клиент с boundary, содержащим ЗАГЛАВНЫЕ (напр.----LV0290), получает 400 «file field required»: delimiter не матчится. Рабочий вариант — lowercase-boundary (----lv0290). Плюс: curl на Windows (mingw) шлёт multipart с LF-разделителями — серверный парсер ждёт CRLF; надёжно — собрать multipart с CRLF вручную (--data-binary). Кандидат на фикс в mappa: не lowercasing'ить boundary / матчить регистронезависимо. - Одноразовый ре-индекс tsvec при создании конфига токенизатора (v0.28.0, 2026-08-28) —
миграция создаёт PG-конфиг
mappa(гейт newly_created) и перестраивает tsvec ВСЕХ сущностей (~13.5k коммитов). Старт после деплоя дольше обычного; health может отвечать 404/не стартовать до ~1 мин — это ожидаемо, не паника. Проверятьdocker ps(health: starting) + повторять /health.