--- 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:` (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: <чекаут victor/tg-digest> docker push registry.kzntsv.site/tg-digest-worker: ``` **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/<дата>-` (конвенция 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/<дата>-`). 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