Files
grabber-ai/docs/docker.md
T

281 lines
21 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.
# Docker: запуск grabber-ai на VPS и сборка в Gitea CI/CD
Контейнер запускает HTTP-сервис (`python -m src.service`): по ссылке на раздачу или по HTML определяет
дорожки озвучки (тип, студия, 18+, язык, роль, параметры аудио). Для трекеров за Cloudflare сервис
обращается к вашему **FlareSolverr**, адрес которого задаётся переменной окружения.
```
клиент ──HTTP──▶ grabber-ai (:8000) ──▶ трекер (rutracker / nnmclub / kinozal)
(grabber) │ ▲ при 403/503 или странице-заглушке
└─▶ FlareSolverr ──┘ (DUB_FLARESOLVERR_URL)
```
Содержание: [быстрый старт](#быстрый-старт) · [переменные окружения](#переменные-окружения) ·
[FlareSolverr](#flaresolverr) · [API](#api) · [модели](#модели-и-как-они-попадают-в-контейнер) ·
[CI/CD в Gitea](#cicd-в-gitea) · [VPS](#развёртывание-на-vps) · [проблемы](#если-что-то-не-работает)
## Что в репозитории
| Файл | Назначение |
|---|---|
| `Dockerfile` | образ: Python 3.12, torch (CPU), код сервиса, при наличии — модели |
| `requirements-service.txt` | зависимости сервиса (без sklearn/pandas/matplotlib/onnx — они нужны только обучению) |
| `docker-compose.yml`, `.env.example` | сервис + FlareSolverr, настройки через `.env` |
| `.dockerignore` | в контекст сборки попадают только `src/`, зависимости и `docker-models/` |
| `docker-models/models/` | сюда `tools/pack_models.py` кладёт модели для образа (в git не попадают) |
| `tools/pack_models.py` | упаковка `models/best` + `models/segmenter` для образа/CI |
| `.gitea/workflows/docker.yml` | сборка, проверка и публикация образа в реестр Gitea |
## Быстрый старт
Нужны Docker и модели (обучены и выбраны через `tools/collect.py`, пункт 7: `models/best`,
`models/segmenter`).
```bash
# 1. положить модели в контекст сборки (образ получится «с моделями»)
python tools/pack_models.py --stage docker-models
# 2. настройки
cp .env.example .env # впишите DUB_API_TOKEN (openssl rand -hex 24)
# 3. сборка и запуск вместе с FlareSolverr
docker compose up -d --build
# 4. проверка
curl http://localhost:8000/health
curl -X POST http://localhost:8000/predict \
-H "Authorization: Bearer <DUB_API_TOKEN>" -H "Content-Type: application/json" \
-d '{"url": "https://rutracker.org/forum/viewtopic.php?t=6894333"}'
```
Первый запуск контейнера грузит модель 5–15 с (`/health` отвечает после этого; healthcheck даёт на
старт до двух минут).
**Образ без моделей.** Если шаг 1 пропустить, образ соберётся пустым по моделям и при старте напишет,
какого каталога не хватает. Тогда смонтируйте модели с хоста — раскомментируйте `volumes` в
`docker-compose.yml`: `./models:/app/models:ro` (внутри нужны `models/best` и `models/segmenter`,
причём у `best` должен быть `config.json` — его создаёт `pack_models.py`; каталог из `pack_models.py
--stage` для этого и подходит: `-v ./docker-models/models:/app/models:ro`).
## Переменные окружения
Всё настраивается переменными (в `.env` для compose или `-e` у `docker run`). Аргументы командной
строки сервиса, если заданы, приоритетнее.
| Переменная | По умолчанию | Назначение |
|---|---|---|
| `DUB_FLARESOLVERR_URL` | не задана (в compose `http://flaresolverr:8191`) | адрес FlareSolverr без `/v1`; пусто — Cloudflare не обходится |
| `DUB_API_TOKEN` | не задан | если задан, `/predict` требует токен (см. [API](#api)); `/health` остаётся открытым |
| `DUB_FETCH_TIMEOUT` | `60` | таймаут загрузки страницы, с. FlareSolverr решает challenge 10–40 с, прежние 15 с не хватало |
| `DUB_TORCH_THREADS` | все ядра | потоков torch; на VPS с несколькими другими сервисами ограничьте |
| `DUB_MODEL_DIR` | `/app/models/best` | каталог основной модели |
| `DUB_SEGMENTER_DIR` | `/app/models/segmenter` | каталог сегментатора страниц; нет каталога — работают правила |
| `DUB_HOST`, `DUB_PORT` | `0.0.0.0`, `8000` | адрес/порт внутри контейнера (наружу — `DUB_PUBLISH_PORT` в compose) |
| `DUB_DEVICE` | авто | `cpu` или `cuda` (в образе только CPU-сборка torch) |
| `DUB_IMAGE` | `grabber-ai:local` | (compose) какой образ запускать, напр. из реестра Gitea |
| `DUB_PUBLISH_PORT` | `8000` | (compose) порт на хосте |
В образе жёстко заданы `HF_HUB_OFFLINE=1` и `TRANSFORMERS_OFFLINE=1`: сервис ничего не скачивает с
Hugging Face. Все файлы моделей, включая архитектуру основы (`config.json`), лежат рядом с весами.
## FlareSolverr
Сервис сначала запрашивает страницу напрямую. Если трекер ответил 403/503 или вернул страницу-заглушку
(Cloudflare «Just a moment», форма входа), запрос повторяется через FlareSolverr
(`POST {DUB_FLARESOLVERR_URL}/v1`, `cmd=request.get`). Без FlareSolverr такие страницы дают ошибку 400
с подсказкой.
Три способа подключить:
1. **Вместе с compose (по умолчанию).** Сервис `flaresolverr` поднимается рядом, порт наружу не
публикуется, `grabber-ai` ходит к нему по `http://flaresolverr:8191`.
2. **Свой FlareSolverr на другом хосте.** В `.env`: `DUB_FLARESOLVERR_URL=http://192.168.0.24:8191`, и
запускайте только `docker compose up -d grabber-ai` (контейнер FlareSolverr не нужен). Адрес должен быть
достижим из контейнера: с VPS до домашней сети — через VPN/туннель, не открывайте FlareSolverr в интернет.
3. **`docker run`:**
```bash
docker run -d --name grabber-ai -p 8000:8000 \
-e DUB_FLARESOLVERR_URL=http://flaresolverr:8191 -e DUB_API_TOKEN=... \
--network <сеть_с_flaresolverr> grabber-ai:local
```
Проверить, что адрес подхватился: `GET /health` → `"flaresolverr": true`. Проверка связи:
`docker compose exec grabber-ai python -c "import urllib.request as u; print(u.urlopen('http://flaresolverr:8191/').read()[:80])"`.
Замечания:
* FlareSolverr нестабилен: бывают `Timeout after 15.0 seconds` и ответ ~40 с. Это ошибка 400/502 для
одного запроса — клиенту стоит повторить запрос.
* kinozal требует авторизацию: передавайте `cookie` в теле запроса (см. ниже), он уйдёт и напрямую, и в
FlareSolverr.
* Сервис ходит только на rutracker, nnmclub и kinozal (список разрешённых хостов в `src/fetch.py`, в том
числе для редиректов): произвольные адреса через `url` не загружаются.
## API
Базовый адрес — `http://<хост>:8000`. Документация Swagger: `/docs`.
### `GET /health`
Без токена. Пример ответа:
```json
{"status": "ok", "model": "/app/models/best", "device": "cpu", "classes": ["авт. двухголосый", "..."],
"segmenter": "model", "flaresolverr": true, "auth": true}
```
`segmenter`: `model` — страницы режет обученный сегментатор, `rules` — запасные правила (нет `models/segmenter`).
### `POST /predict`
Ровно одно из полей тела:
| Поле | Что делает |
|---|---|
| `url` | сервис загружает страницу раздачи (с FlareSolverr при необходимости) и разбирает её; ответ дополнен `tracker` |
| `html` | разбор страницы, которую вы уже скачали (все дорожки релиза) |
| `text` | одна строка-дорожка: `«Перевод: Дублированный \| MovieDalen»` |
Необязательно: `title` (заголовок раздачи для `html`), `cookie` (для `url`, строка `a=b; c=d`).
Токен (если задан `DUB_API_TOKEN`): `Authorization: Bearer <токен>` либо `X-API-Key: <токен>`.
```bash
curl -s -X POST http://localhost:8000/predict \
-H "Authorization: Bearer $DUB_API_TOKEN" -H "Content-Type: application/json" \
-d '{"url": "https://rutracker.org/forum/viewtopic.php?t=6894333"}'
```
Ответ: `{"tracks": [...], "studios": [...], "is_18plus": bool, "tracker": "...", ...}`. Поля дорожки
(тип, студия, 18+, язык, роль, аудио-параметры, эпизоды) описаны в
[integration.md](integration.md) и [inference.md](inference.md).
| Код | Когда |
|---|---|
| 200 | успех |
| 400 | не то число полей; страница недоступна/заглушка (Cloudflare без FlareSolverr, FlareSolverr не справился); ссылка не с поддерживаемого трекера |
| 401 | задан `DUB_API_TOKEN`, а токена нет или он неверный |
| 502 | сеть: страницу не удалось загрузить |
## Модели и как они попадают в контейнер
Модели весят сотни МБ (`models/best` ≈ 700 МБ, `models/segmenter` ≈ 115 МБ), в git они не лежат
(`models/*` в `.gitignore`). Варианты:
| Способ | Плюсы | Минусы |
|---|---|---|
| **А. Запечь в образ** (`pack_models.py --stage docker-models`, в CI — через `MODELS_URL`) | образ самодостаточен, версия модели = тег образа | образ ≈ 2 ГБ, смена модели = новая сборка и pull |
| **Б. Смонтировать с хоста** (`-v ./models:/app/models:ro`) | образ лёгкий, модель меняется без пересборки (перезапуск контейнера) | модель нужно положить на VPS отдельно |
Для CI удобнее А. Чтобы CI знал модели, их нужно один раз положить в место, откуда он их скачает.
### Упаковка и выкладка в Gitea (Generic Packages)
```bash
python tools/pack_models.py # -> models-bundle.tar.gz (≈ 750 МБ)
# выкладка в реестр пакетов Gitea (версия — любая строка, например дата или хэш модели)
curl --fail -H "Authorization: token <GITEA_TOKEN>" \
--upload-file models-bundle.tar.gz \
https://<gitea>/api/packages/<owner>/generic/grabber-ai-models/2026-10-28/models-bundle.tar.gz
```
Ссылка для скачивания (она же `MODELS_URL`):
`https://<gitea>/api/packages/<owner>/generic/grabber-ai-models/2026-10-28/models-bundle.tar.gz`.
Если у вашей Gitea ограничен размер пакета (`[packages] LIMIT_SIZE_GENERIC`, по умолчанию без лимита, но
nginx перед ней часто режет загрузки ~1 МБ), поднимите лимиты или используйте способ Б.
Новая модель: обучить → выбрать в меню (пункт 7) → `pack_models.py` → выложить под новой версией →
обновить секрет `MODELS_URL` → запустить workflow (Actions → docker → Run workflow).
## CI/CD в Gitea
Workflow `.gitea/workflows/docker.yml` собирает образ и публикует его в **реестр контейнеров Gitea**
(`<gitea-host>/<owner>/<repo>/grabber-ai`).
### Что настроить один раз
1. **Actions и раннер.** В Gitea включены Actions (`[actions] ENABLED=true`), в репозитории или
организации зарегистрирован `act_runner`. Раннеру нужен Docker: самый простой вариант — раннер на
хосте с метками вида `ubuntu-latest:host` (команды `docker build` идут на Docker хоста) или раннер в
контейнере с примонтированным `/var/run/docker.sock`. Образ-раннер (`ubuntu-latest:docker://…`) без
доступа к Docker daemon собрать образ не сможет.
2. **Secrets** (Settings → Actions → Secrets):
| Секрет | Значение |
|---|---|
| `ACTION_PAT` | токен Gitea (Settings → Applications → Access Tokens) с правами `write:package` и `read:package`; им образ публикуется в реестр и скачиваются модели. Имена секретов не могут начинаться с `GITEA_` и `GITHUB_`, поэтому не `GITEA_TOKEN` |
| `REGISTRY_USER` | необязательно: владелец токена; по умолчанию — владелец репозитория (`denozord`) |
| `MODELS_URL` | необязательно: ссылка на `models-bundle.tar.gz`; нет — образ без моделей |
3. Адрес реестра — `git.shx.one` (HTTPS), он задан в workflow (`REGISTRY_HOST`); другой можно указать
переменной репозитория `REGISTRY_HOST`. Адрес берётся не из `GITHUB_SERVER_URL`: внутри раннера это
внутренний `http://192.168.x.x:3000` без HTTPS, и `docker login` падает с «server gave HTTP response
to HTTPS client». Если реестр всё же по HTTP, добавьте его в `insecure-registries` Docker
(`/etc/docker/daemon.json`).
### Что делает workflow
| Шаг | Действие |
|---|---|
| проверка | `compileall` кода сервиса |
| модели | при заданном `MODELS_URL` скачивает архив в `docker-models/`, проверяет `models/best/model.pt` |
| теги | `sha-<8 символов коммита>`; на тег `vX.Y.Z` — ещё и `vX.Y.Z`; на `main`/`master` — ещё и `latest` |
| сборка | `docker build` |
| проверка образа | если модели внутри — запускает контейнер и ждёт ответ `/health` (до 5 минут) |
| публикация | `docker login` и `docker push` всех тегов |
Запуск: пуш в `main`/`master`, тег `v*` или вручную. Релиз: `git tag v1.0.0 && git push --tags`.
## Развёртывание на VPS
1. Установить Docker и Compose, залогиниться в реестр Gitea:
```bash
docker login <gitea-host> -u <user> # токен с read:package
```
2. Положить на VPS `docker-compose.yml` и `.env` (из `.env.example`):
```
DUB_IMAGE=<gitea-host>/<owner>/<repo>/grabber-ai:latest
DUB_API_TOKEN=<openssl rand -hex 24>
DUB_FLARESOLVERR_URL=http://flaresolverr:8191
```
В `docker-compose.yml` строка `build: .` на VPS не нужна — можно удалить.
3. `docker compose pull && docker compose up -d`.
4. Обновление: после новой сборки в CI — `docker compose pull && docker compose up -d`.
**Ресурсы.** CPU-инференс; модель ruBert-base в fp32 (~0,7 ГБ) + сегментатор + torch — ожидайте
порядка 1,5–2 ГБ памяти (точный замер на VPS не делался; в compose стоит лимит 3 ГБ). Скорость: разбор
страницы раздачи на моём ПК при 2 потоках CPU — около 0,9 с (на VPS зависит от процессора). Загрузка по
`url` добавляет время сети и, при Cloudflare, 10–40 с FlareSolverr. Нужны ≥ 2 vCPU и ≥ 3 ГБ RAM; на
1 ГБ контейнер не поместится — тогда обучите и используйте `tiny2` (≈ 117 МБ весов; для него
`DUB_MODEL_DIR` указывает на каталог этой модели).
**Безопасность.**
* Не публикуйте `:8000` без токена; лучше поставить перед сервисом reverse-proxy с TLS (Caddy:
`dub.example.com { reverse_proxy 127.0.0.1:8000 }` — сертификат выдаётся автоматически) и закрыть порт
файрволом. Токен по HTTP передаётся открытым текстом.
* FlareSolverr (`:8191`) наружу не открывайте: у него нет аутентификации, он умеет открывать любые URL.
* Контейнер работает от непривилегированного пользователя, модели монтируются только на чтение.
## Если что-то не работает
| Симптом | Причина и решение |
|---|---|
| Контейнер завершился: `нет модели: /app/models/best/model.pt` | образ без моделей и каталог не смонтирован — см. [модели](#модели-и-как-они-попадают-в-контейнер) |
| Ошибка про `config.json` / доступ к huggingface.co при старте | у `models/best` нет `config.json` (старый чекпоинт); соберите через `tools/pack_models.py` — он добавит его |
| `/predict` → 400 «страница недоступна: Cloudflare challenge … (задайте FlareSolverr)» | не задан `DUB_FLARESOLVERR_URL` или контейнер его не видит (`/health` → `flaresolverr`), проверьте сеть/имя сервиса |
| 400 `FlareSolverr: … Timeout` | FlareSolverr не успел решить challenge; повторите запрос, увеличьте `DUB_FETCH_TIMEOUT`, перезапустите FlareSolverr |
| 401 | не передан токен или он отличается от `DUB_API_TOKEN` |
| Контейнер `unhealthy` в первые минуты | модель ещё грузится; `docker logs grabber-ai`. Если не проходит — не хватает памяти (`docker stats`) |
| CI: `Cannot connect to the Docker daemon` | у раннера нет доступа к Docker (см. «Что настроить один раз») |
| CI: `server gave HTTP response to HTTPS client` | workflow взял внутренний адрес Gitea; должен быть `REGISTRY_HOST` = `git.shx.one` (уже так в workflow) |
| CI: `unauthorized` при push | у `ACTION_PAT` нет `write:package` или токен принадлежит не владельцу/участник пространства имён репозитория |
| CI: образ без моделей, хотя ждали с ними | секрет `MODELS_URL` пуст или шаг скачивания пропущен — смотрите лог шага «Модели» |
## Что проверено, а что нет
Проверено на ПК разработчика: сервис запускается без sklearn/matplotlib/pandas/onnx; модель грузится
полностью офлайн (`HF_HUB_OFFLINE=1`) из каталога, собранного `pack_models.py`, и даёт те же ответы, что
и обычная загрузка; токен (Bearer / X-API-Key), `DUB_FETCH_TIMEOUT`, `/health`; YAML workflow и compose
разбираются; тесты проекта проходят.
**Не проверено: `docker build`, запуск контейнера и workflow в Gitea** — на машине разработчика Docker не
установлен, а Gitea-репозитория ещё нет. Первая сборка может потребовать правок (например, имя метки
раннера, доступность колёс torch для вашей архитектуры: используется
`https://download.pytorch.org/whl/cpu`, `torch==2.11.0`, Python 3.12, x86_64 и aarch64).