# Развёртывание платформы Одна VM, systemd, бинари артефактами CI (`PLATFORM_DIRECTION.md` §3). Не Kubernetes: дети-`tmctl` живут часами и держат эксклюзивный лок на файлах книги на локальном диске — оркестратор, способный переселить под посреди прогона, этой нагрузке враждебен. ## Файлы - `tmplatformd.service` — юнит контрольной панели. Тело юнита проверено `systemd-analyze verify` (systemd 259) — exit 0, без замечаний. ⚠ Проверять надо с ПОДСТАВЛЕННЫМ существующим `ExecStart=`: дословно юнит даёт exit 1, потому что `verify` проверяет и наличие бинаря, а `/usr/local/bin/tmplatformd` на стенде нет. ⚠ **юнит целиком под systemd не запускался** — нет sudo и нет бинаря на стенде. Отдельные свойства песочницы при этом проверены ЖИВЫМ прогоном в пользовательском systemd 259 (`systemd-run --user --wait`), а не вычитаны из доки: `ProtectSystem=strict` + `ReadWritePaths=` на несуществующем пути → **`226/NAMESPACE`, юнит не стартует**; тот же путь созданным → `0/SUCCESS`; префикс `-` на несуществующем → `0/SUCCESS` (строка игнорируется — потому мы её и не префиксуем); `ProtectHome=yes` → `/home` пуст, чтение `/home/` даёт `Permission denied`; `ProtectHome=tmpfs` + `BindPaths=<каталог>` → каталог виден. Ресурсные потолки (`MemoryMax=80%`, `OOMPolicy=continue`) живьём не проверялись — вывод из `systemd.resource-control(5)`/`systemd.service(5)`. ## Откат релиза: не ниже версии 5 `goose down` до версии 4 и ниже НЕ РАБОТАЕТ на живой базе: down-путь `00005` восстанавливает `users_email_key` и `email NOT NULL`, а обе формы нарушают строки, которые пишет боевой код (неподтверждённая личность даёт `email = NULL`; один адрес законно принадлежит двум аккаунтам). Откат транзакционный, поэтому падение ничего не портит — но планировать откат ниже 5 нельзя, план отката — накатить вперёд. Разбор: `docs/STACK_DECISIONS.md` §8. ## Что юнит закрывает содержательно - ⚠ **ПЕРЕ-ДИСПОЗИЦИЯ (P4, D39.106): прогон НЕ ребёнок этого юнита и не живёт в его cgroup.** Каждый прогон — транзиентный юнит в СОБСТВЕННОМ systemd-менеджере пользователя `tmplatform`, в срезе `tm-runs.slice`, потому что перевод обязан пережить деплой платформы. Следствия для установки: обязателен `loginctl enable-linger tmplatform`; юниту нужен доступ к своей же пользовательской шине (`ProtectHome=tmpfs` + `BindPaths=/run/user/%U` + `DBUS_SESSION_BUS_ADDRESS`); ресурсные потолки НИЖЕ ограничивают только контрол-плейн. - **PD-13 (осиротевшие процессы движка)** закрыт теперь ТАМ, где прогон: собственным cgroup прогона (`TM_PLATFORM_RUN_MEMORY_MAX`/`_TASKS_MAX`), и это ИЗМЕРЕНО — в дефолтном `app.slice` пользовательского менеджера лимиты принимаются и не применяются (`STACK_DECISIONS` §16). Дев-путь супервизора с группой процессов остаётся дев-путём. - **`TimeoutStopSec=90`** больше, чем дренаж платформы (15 с) плюс grace движка (30 с). Меньше — и systemd прибьёт `tmctl` посреди остановки, оставив лок проекта. - **Секреты через `LoadCredential=`,** а не через окружение: переменная окружения видна в `/proc//environ` и наследуется каждым ребёнком-`tmctl`. Конфиг читает `*_FILE` первым. ## Установка (набросок, исполняется владельцем) ```sh useradd --system --home-dir /srv/textmachine tmplatform # ⚠ Каталог создаём ЯВНО: --system не создаёт домашний каталог, а `ReadWritePaths=/srv/textmachine` # при `ProtectSystem=strict` на несуществующем пути валит сборку mount-namespace — юнит не стартует # вовсе (PD-91, systemd.exec(5)). Это первая команда, которую пропускают, читая набросок сверху вниз. install -d -m0750 -o tmplatform -g tmplatform /srv/textmachine install -D -m0755 tmplatformd /usr/local/bin/tmplatformd install -D -m0755 tmplatformctl /usr/local/bin/tmplatformctl # ⚠ БЕЗ ЭТОГО НИ ОДИН ПРОГОН НЕ СТАРТУЕТ. Прогоны — транзиентные юниты в пользовательском # менеджере `tmplatform`, а он существует вне сессии входа только при включённом linger. # Проверка: `systemctl --user -M tmplatform@ is-system-running` отвечает, а не «Failed to connect». loginctl enable-linger tmplatform # Движок кладётся по ВЕРСИОНИРОВАННОМУ пути: попытка прогона пиннится к тому, с которого началась # (строка 139), поэтому выкат новой версии не подменяет исполняемое у идущего прогона. install -D -m0755 tmctl /opt/textmachine/engine/<версия>/tmctl install -d -m0700 -o root -g root /etc/tmplatform printf '%s' 'postgres://...' > /etc/tmplatform/dsn && chmod 0400 /etc/tmplatform/dsn printf '%s' '' > /etc/tmplatform/oidc_client_secret && chmod 0400 /etc/tmplatform/oidc_client_secret ``` `/etc/tmplatform/env` — несекретное окружение: ``` TM_PLATFORM_ADDR=127.0.0.1:8080 TM_PLATFORM_TRUSTED_ORIGINS=https://app.example.org TM_PLATFORM_OIDC_ISSUER=https://accounts.google.com TM_PLATFORM_OIDC_CLIENT_ID=... TM_PLATFORM_OIDC_REDIRECT_URL=https://app.example.org/auth/callback TM_PLATFORM_AFTER_LOGIN=/library TM_PLATFORM_SIGNUP_GRANT_USD=5 ``` ⚠ **`TM_PLATFORM_STATE_DIR` меняется только когда живых прогонов нет.** Exit-маркер пишется по пути, вычисленному при СПАВНЕ, а читается по пути из текущей конфигурации: после смены каталога конец идущего прогона становится невидим, и реконсилятор перезапускает его как потерянный (PD-155). Путь обязан быть абсолютным — относительный демон отвергает на старте, потому что маркер пишет юнит из каталога книги, а читает демон из своего. Миграции выкатываются ОДИН раз, не каждой репликой: `TM_PLATFORM_MIGRATE=1 tmplatformd` разово либо отдельный шаг деплоя. `goose` держит advisory-лок, так что параллельный запуск не гонка, но и не норма. ## Где на сервере лежат книги `/srv/textmachine` — корень библиотеки НА СЕРВЕРЕ. Решение владельца «книги живут в `~/books`» относится к машине разработки: под этим юнитом домашние каталоги недоступны вовсе (`ProtectHome=yes` подставляет пустой `/home` и детям-`tmctl` тоже), поэтому библиотека под домашним каталогом на сервере просто не откроется — не «сработает медленнее», а не найдётся. Оператору, которому это нужно, юнит называет ровно две строки замены (`ProtectHome=tmpfs` + `BindPaths=`); других изменений не требуется. ⚠ Выбирающего путь кода ещё нет: `Supervisor.Workdir` задаёт вызывающий, а вызывающий — воркер, которого нет (строка 103 единого бэклога). Когда он появится, корень становится настройкой, и её дефолт — этот каталог. ## Чего здесь ещё нет TLS и домен (перед юнитом предполагается edge-прокси), ограничитель соединений на edge, ротация логов, бэкап Postgres. Всё это — работа с первым реальным деплоем, не раньше.