docker / build (push) Successful in 1m26s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
283 lines
22 KiB
Markdown
283 lines
22 KiB
Markdown
# 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)); если нет — 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` **необязателен**. Описание большинства раздач открывается без входа, и запрос
|
||
`{"url": "https://kinozal.me/details.php?id=…"}` работает как есть. Если сервис ответил `400` про форму
|
||
входа (раздача закрыта для гостей), передайте `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>/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>/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).
|