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

12 KiB
Raw Blame History

title, type, tags, related, updated
title type tags related updated
tg-digest — VDS deploy runbook (sched cron + HTTP-воркер) concept
tg-digest
sched
vds
deploy
docker
portainer
runbook
worker
telegram
concepts/runbooks-index.md
concepts/sched-vds-deploy-runbook.md
concepts/portainer-stack-management-vds.md
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)

{
  "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 образа:

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):

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 (ручной триггер):

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, если не передать массив целиком).

Связи