Files
admin/.wiki/concepts/tg-digest-vds-deploy-runbook.md
vitya 9da36ddb27 docs(runbook): tg-digest VDS deploy runbook + stack artifacts (task:1311)
Ранбук первого деплоя паттерна sched+worker (модель для yt-digest): стек tg-digest (воркер internal, mem_limit 256m), runtime-регистрация задачи POST /tasks, env-контракт, smoke/verify/rollback, gotchas (сессия Telethon, TZ UTC, timeoutMs). Артефакты: compose source-of-truth, Dockerfile + http_worker.py (черновики для коммита в victor/tg-digest). task:1311 разблокирован (1305 done) -> ready.
2026-08-28 23:33:47 +03:00

169 lines
12 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.
---
title: "tg-digest — VDS deploy runbook (sched cron + HTTP-воркер)"
type: concept
tags: [tg-digest, sched, vds, deploy, docker, portainer, runbook, worker, telegram]
related: [concepts/runbooks-index.md, concepts/sched-vds-deploy-runbook.md, concepts/portainer-stack-management-vds.md]
updated: 2026-08-28
---
# tg-digest — VDS deploy runbook (sched cron + HTTP-воркер)
> **⚙️ СЛУЖЕБНЫЙ РАНБУК — только для администратора проекта `.admin`.**
> Применять/выполнять шаги может только **`.admin`** (оператор проекта admin). Другим проектам/агентам — читать по запросу, не выполнять.
> Из проекта не выносить: не копировать в другие вики, не пересказывать, не публиковать.
> Нужен деплой или прод-операция по этому ранбуку — **поставить задачу админу (`.admin`) и написать письмо**. Админ выполняет, остальные верифицируют.
Развёртывание **воркера tg-digest** (дневной дайджест телеграм-каналов) на инфра-VDS:
контейнер-воркер (internal, сеть `proxy`, без traefik-роута) + задача в **sched** (ядро уже
задеплоено, стек 28, см. `sched-vds-deploy-runbook`): cron `0 5 * * *`, runner http, simple mode.
Опер-таска: `task:1311` (tg-digest-ops-stack). Аналог: yt-digest `task:1190` (не задеплоен —
этот ранбук первый для паттерна sched+worker).
## Параметры
| Что | Значение |
|---|---|
| Хост | VDS kzntsv `89.253.255.94` |
| Стек Portainer | **`tg-digest`** (новый, создаётся первым деплоем), endpointId **1**; после создания id записать сюда |
| Образ | `registry.kzntsv.site/tg-digest-worker:<tag>` (python:3.13-alpine, telethon+requests) |
| Compose source-of-truth | `admin/host-stacks/vds-kzntsv/tg-digest.compose.yml` |
| Воркер | контейнер `tg-digest`, HTTP `:8080`: `GET /healthz`, `POST /run` (simple mode: 2xx = ok) |
| Задача sched | runtime-регистрация `POST /tasks` (fileManaged:false, без рестарта ядра); cron `0 5 * * *` (**tz UTC** = 08:00 MSK) |
| Sched admin key | `pass sched/admin-key` (Bearer) |
| Per-task auth | `x-sched-api-key` = `WORKER_API_KEY` воркера = `pass sched/tg-digest-api-key` (завести при деплое) |
| mem_limit | `256m` |
| Сети | `proxy` (external, internal — НЕТ traefik-labels, наружу не публикуется) |
| Registry | `REGISTRY_URL/REGISTRY_USER/REGISTRY_PASS` = `pass vds-kzntsv/full-env` |
## Артефакты
- **Код:** репо victor/tg-digest, ветка main, минимум `d3aefb2` (raw-ингест, task:1475 —
старый `ingest_wiki` удалён, per-post ингест не работает). Воркер: `python -m src.worker`.
- **Dockerfile + HTTP-обёртка** (`http_worker.py`): коммитятся в **корень репо** victor/tg-digest
(dev-сторона); черновики — `admin/host-stacks/vds-kzntsv/tg-digest/`. Без них image не собрать.
- **Env-контракт воркера** (см. таблицу ниже) — все значения env (pass в контейнере НЕТ).
### Env-контракт (воркер)
| Переменная | Источник (pass) | Обязательна | Назначение |
|---|---|---|---|
| `TG_API_ID` | `telegram/api-id`**НЕТ в pass, оператор** | да | MTProto app id (my.telegram.org) |
| `TG_API_HASH` | `telegram/api-hash`**НЕТ в pass, оператор** | да | MTProto app hash |
| `TG_SESSION` | `/data/session.session` (volume; создаётся интерактивным логином) | да | Telethon-сессия |
| `TG_PHONE` | оператор (только при первом логине) | первый запуск | телефон аккаунта |
| `TG_CACHE` | — (дефолт `tg-cache`; на деплое `/data/tg-cache`) | опц | кэш коллектора + state.json (персист) |
| `TG_WINDOW_HOURS` | — (дефолт 24; бэкфилл 26-27 → 48-72) | опц | окно сбора |
| `TELEGRAM_BOT_TOKEN` | `telegram/full-env` | да | доставка дайджеста |
| `TELEGRAM_CHAT_ID` | `telegram/full-env` | да | чат доставки |
| `LLM_API_KEY` | pass-запись **отсутствует — уточнить у оператора** (deepseek; локально в gitignored `src/config/llm.json`) | да | стадия-2 (LLM-находки) |
| `LLM_BASE_URL` | — (дефолт `https://api.deepseek.com`) | опц | |
| `LLM_MODEL` | — (дефолт `deepseek-chat`) | опц | |
| `MAPPA_API_TOKEN` | `mappa/full-env` | да | ингест raw (wiki:3312) |
| `MAPPA_URL` | — (дефолт `https://mappa.vds.kzntsv.site`) | опц | |
| `MAPPA_PROJECT` | — (дефолт `tg-digest`) | опц | |
| `WORKER_API_KEY` | `sched/tg-digest-api-key` (завести) | да | проверка `x-sched-api-key` в /run |
| `TG_SEND` | — (`0` — dry-run smoke без отправки) | опц | доставка |
### Задача sched (POST /tasks, runtime)
```json
{
"name": "tg-digest",
"schedules": [{ "cron": "0 5 * * *" }],
"config": {
"url": "http://tg-digest:8080/run",
"method": "POST",
"timeoutMs": 900000,
"auth": { "apiKey": "<из pass sched/tg-digest-api-key>" }
}
}
```
Простой режим (без `envelope`): sched шлёт POST, ждёт ответ до `config.timeoutMs`, 2xx = succeeded.
**`task.timeoutMs` НЕ ставить** (ceiling шлёт воркеру cancel — воркер его не реализует).
## Первый деплой
**0. Предусловия (оператор):**
- `pass telegram/api-id` + `pass telegram/api-hash` (my.telegram.org → App `tg-digest`);
- **сессия Telethon**: первый `python -m src.collector --live` интерактивен (телефон + код из СМС) —
создать session-файл заранее, положить в volume (см. gotcha 1);
- `LLM_API_KEY` (deepseek) — pass-запись или env;
- `pass sched/tg-digest-api-key` (новая запись, ключ для `x-sched-api-key`).
**1. Dockerfile + http_worker.py в репо** (корень victor/tg-digest), main. Билд строго из main.
**2. Build + push образа:**
```bash
docker login $REGISTRY_URL -u $REGISTRY_USER -p "$(pass show vds-kzntsv/full-env | awk -F= '/^REGISTRY_PASS=/{print $2}')"
docker build -t registry.kzntsv.site/tg-digest-worker:<tag> <чекаут victor/tg-digest>
docker push registry.kzntsv.site/tg-digest-worker:<tag>
```
**3. Стек Portainer** (канон `portainer-stack-management-vds`): создать `tg-digest`
(`POST /api/stacks/create/standalone/string?endpointId=1`, stackFileContent = compose из
source-of-truth, env-массив = секреты из таблицы; `pullImage: true`). **Без traefik-labels**
(воркер internal, доступа снаружи нет).
**4. Регистрация задачи (без рестарта sched):**
```bash
AK=$(pass show sched/admin-key)
curl -ksS -X POST https://sched.vds.kzntsv.site/api/tasks \
-H "Authorization: Bearer $AK" -H 'Content-Type: application/json' \
-d '{"name":"tg-digest","schedules":[{"cron":"0 5 * * *"}],
"config":{"url":"http://tg-digest:8080/run","method":"POST","timeoutMs":900000,
"auth":{"apiKey":"<из pass sched/tg-digest-api-key>"}}}'
```
**5. Smoke (ручной триггер):**
```bash
curl -ksS -X POST https://sched.vds.kzntsv.site/api/tasks/tg-digest/run \
-H "Authorization: Bearer $AK" -H 'Content-Type: application/json' \
-d '{"temporary": true, "triggeredBy": "admin-smoke"}'
```
Блокируется до конца рана (sync runner) → ответ = терминальный run.
Сухой деплой без ключей: контейнер Up, `/healthz` 200, ран падает `die 2 creds missing` — ок как
проверка плумбинга, не как acceptance.
## Verify
- Контейнер: `Up (healthy)`; `docker exec tg-digest python -c "import urllib.request;print(urllib.request.urlopen('http://127.0.0.1:8080/healthz').read())"``{"ok": true}`.
- sched: `GET /api/tasks` (Bearer) → `tg-digest` в списке, `nextRunAt` завтра 05:00 UTC.
- Smoke run: `GET /api/runs?task=tg-digest` → последний run `succeeded` (с ключами) с полем `result`.
- Артефакт дня: raw-запись в mappa `raw/research/<дата>-<topic>` (конвенция wiki:3312) + доставка в TG.
## Rollback
- Задача: `DELETE /api/tasks/tg-digest` (runtime-таска, 204) — расписание снято.
- Стек: удалить `tg-digest` в Portainer.
- Образ: старый тег в registry (повторный деплой с предыдущим тегом).
## Gotchas
1. **Сессия Telethon обязательна, api_id/api_hash недостаточны** (2026-08-28): первый `--live`
интерактивен — телефон + СМС-код; без session-файла в контейнере коллектор не стартует.
Сессию создать заранее (локально или TTY контейнера), смонтировать в `/data/session.session`.
2. **pass в контейнере НЕТ** (2026-08-28): коллектор умеет читать pass через bash (`_pass`), но в
образе его нет → только env. Все секреты — env при деплое (chmod 600 / Portainer env, вне git).
3. **TZ cron = UTC** (дефолт sched): `0 5 * * *` = 08:00 MSK (утренний дайджест, как yt-digest).
4. **`config.timeoutMs` (транспортный) — 900000** (пайплайн collect→stage2 LLM→ingest→tg занимает
минуты); `task.timeoutMs` (ceiling) НЕ ставить — при срабатывании sched шлёт `POST /cancel`,
воркер (не паттерн C, не @apilki) его не реализует → ран пометится failed при живом воркере.
5. **mem_limit 256m**: python+telethon на старте ~150-200m — достаточно; не поднимать без нужды.
6. **Образ строго из main с `d3aefb2`** (raw-ингест): старый `ingest_wiki` удалён, per-post
ингест не работает; билд из старого коммита → падение на ингесте.
7. **Бэкфилл за 26-27 августа**: `TG_WINDOW_HOURS` 48-72 на один прогон (или два прогона);
`ingest_raw` идемпотентен по дню (`raw/<дата>-<topic>`).
8. **Runtime-регистрация = fileManaged:false**: `POST /tasks` не трогает `sched.tasks.json`
(остаётся `{"tasks": []}`); таска живёт до `DELETE /tasks/:name`. tasks.json — только
декларативный источник (рестарт ядра НЕ нужен).
9. **Portainer env не env_file** — секреты в env-массив стека (gotcha 4b портайнер-канона:
PUT сбрасывает env, если не передать массив целиком).
## Связи
- [[concepts/runbooks-index.md]] — индекс ранбуков
- [[concepts/sched-vds-deploy-runbook.md]] — ядро sched (стек 28, admin API, MariaDB)
- [[concepts/portainer-stack-management-vds.md]] — общий Portainer-канон (JWT, gotchas)
- Репо victor/tg-digest: `src/worker.py` (оркестратор), спека wiki:3260, конвенция raw wiki:3312