Files
grabber-ai/docs/docker.md
T

22 KiB
Raw Blame History

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 · API · модели · CI/CD в Gitea · 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).

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

Без токена. Пример ответа:

{"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: <токен>.

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

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