# Docker: запуск DUB-AI на VPS и сборка в Gitea CI/CD Контейнер запускает HTTP-сервис (`python -m src.service`): по ссылке на раздачу или по HTML определяет дорожки озвучки (тип, студия, 18+, язык, роль, параметры аудио). Для трекеров за Cloudflare сервис обращается к вашему **FlareSolverr**, адрес которого задаётся переменной окружения. ``` клиент ──HTTP──▶ dub-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 " -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` | `dub-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` поднимается рядом, порт наружу не публикуется, `dub-ai` ходит к нему по `http://flaresolverr:8191`. 2. **Свой FlareSolverr на другом хосте.** В `.env`: `DUB_FLARESOLVERR_URL=http://192.168.0.24:8191`, и запускайте только `docker compose up -d dub-ai` (контейнер FlareSolverr не нужен). Адрес должен быть достижим из контейнера: с VPS до домашней сети — через VPN/туннель, не открывайте FlareSolverr в интернет. 3. **`docker run`:** ```bash docker run -d --name dub-ai -p 8000:8000 \ -e DUB_FLARESOLVERR_URL=http://flaresolverr:8191 -e DUB_API_TOKEN=... \ --network <сеть_с_flaresolverr> dub-ai:local ``` Проверить, что адрес подхватился: `GET /health` → `"flaresolverr": true`. Проверка связи: `docker compose exec dub-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 " \ --upload-file models-bundle.tar.gz \ https:///api/packages//generic/dub-ai-models/2026-10-28/models-bundle.tar.gz ``` Ссылка для скачивания (она же `MODELS_URL`): `https:///api/packages//generic/dub-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** (`///dub-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. Реестр контейнеров Gitea доступен по HTTPS. Если Gitea по HTTP, добавьте её адрес в `insecure-registries` Docker на раннере и на VPS (`/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 -u # токен с read:package ``` 2. Положить на VPS `docker-compose.yml` и `.env` (из `.env.example`): ``` DUB_IMAGE=///dub-ai:latest DUB_API_TOKEN= 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 dub-ai`. Если не проходит — не хватает памяти (`docker stats`) | | CI: `Cannot connect to the Docker daemon` | у раннера нет доступа к Docker (см. «Что настроить один раз») | | 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).