Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
21 KiB
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 · 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); /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
с подсказкой.
Три способа подключить:
- Вместе с compose (по умолчанию). Сервис
flaresolverrподнимается рядом, порт наружу не публикуется,dub-aiходит к нему поhttp://flaresolverr:8191. - Свой FlareSolverr на другом хосте. В
.env:DUB_FLARESOLVERR_URL=http://192.168.0.24:8191, и запускайте толькоdocker compose up -d dub-ai(контейнер FlareSolverr не нужен). Адрес должен быть достижим из контейнера: с VPS до домашней сети — через VPN/туннель, не открывайте FlareSolverr в интернет. docker run: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
Без токена. Пример ответа:
{"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/dub-ai-models/2026-10-28/models-bundle.tar.gz
Ссылка для скачивания (она же MODELS_URL):
https://<gitea>/api/packages/<owner>/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
(<gitea-host>/<owner>/<repo>/dub-ai).
Что настроить один раз
-
Actions и раннер. В Gitea включены Actions (
[actions] ENABLED=true), в репозитории или организации зарегистрированact_runner. Раннеру нужен Docker: самый простой вариант — раннер на хосте с метками видаubuntu-latest:host(командыdocker buildидут на Docker хоста) или раннер в контейнере с примонтированным/var/run/docker.sock. Образ-раннер (ubuntu-latest:docker://…) без доступа к Docker daemon собрать образ не сможет. -
Secrets (Settings → Actions → Secrets):
Секрет Значение ACTION_PATтокен Gitea (Settings → Applications → Access Tokens) с правами write:packageиread:package; им образ публикуется в реестр и скачиваются модели. Имена секретов не могут начинаться сGITEA_иGITHUB_, поэтому неGITEA_TOKENREGISTRY_USERнеобязательно: владелец токена; по умолчанию — владелец репозитория ( denozord)MODELS_URLнеобязательно: ссылка на models-bundle.tar.gz; нет — образ без моделей -
Адрес реестра —
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-registriesDocker (/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
- Установить Docker и Compose, залогиниться в реестр Gitea:
docker login <gitea-host> -u <user> # токен с read:package - Положить на VPS
docker-compose.ymlи.env(из.env.example):ВDUB_IMAGE=<gitea-host>/<owner>/<repo>/dub-ai:latest DUB_API_TOKEN=<openssl rand -hex 24> DUB_FLARESOLVERR_URL=http://flaresolverr:8191docker-compose.ymlстрокаbuild: .на VPS не нужна — можно удалить. docker compose pull && docker compose up -d.- Обновление: после новой сборки в 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: 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).