Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

696
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,696 @@
name: vidconf
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-vidconf}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-vidconf}
POSTGRES_DB: ${POSTGRES_DB:-vidconf}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-vidconf} -d ${POSTGRES_DB:-vidconf}"]
interval: 5s
timeout: 5s
retries: 10
logging: *default-logging
# NOTE: btree_gist extension is created by an Alembic migration, not here.
redis:
image: redis:7
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
logging: *default-logging
backend:
build:
context: ../backend
dockerfile: Dockerfile
restart: unless-stopped
env_file:
- ../.env
environment:
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml}
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-change-me-livekit-secret}
LIVEKIT_PUBLIC_URL: ${LIVEKIT_PUBLIC_URL:-ws://localhost:7880}
# Внутренний server-to-server адрес LiveKit (RoomService/Egress API).
# Жёстко перекрывает значение из .env: там LIVEKIT_URL=ws://localhost:7880
# для запуска backend на хосте, а изнутри контейнера localhost — это сам
# backend, и start_track_egress молча деградирует (warning, записи нет).
LIVEKIT_URL: ws://livekit:7880
# Общий с egress/worker-transcriber путь на volume `recordings` —
# backend формирует по нему filepath для start_track_egress, сам
# том backend'у не примонтирован (файлы пишет контейнер egress).
RECORDINGS_DIR: ${RECORDINGS_DIR:-/recordings}
# Каталог загруженных аватаров — общий volume с nginx, который
# раздаёт его напрямую по `location /media/` (alias), в обход backend.
MEDIA_ROOT: ${MEDIA_ROOT:-/app/media}
# Версия инстанса (релиз v0.0.1) — install.sh копирует значение
# из файла VERSION (корень репозитория) в .env; отдаётся в GET /api/health.
VIDCONF_VERSION: ${VIDCONF_VERSION:-0.0.1}
# config/ лежит в корне репозитория и не попадает в образ (контекст сборки —
# только backend/), поэтому plugins.yaml монтируется отдельно.
volumes:
- ../config:/app/config:ro
- media:/app/media
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
interval: 10s
timeout: 5s
retries: 10
start_period: 15s
logging: *default-logging
# --- Celery worker + beat: та же backend-сборка, но с примонтированным
# пакетом workers/ (workers/ импортирует модели backend). ---
# `-Q celery,summarize,notify`: на малых пресетах
# поставки (13) один контейнер обслуживает суммаризацию, уведомления и
# обслуживающие задачи (`celery` — дефолтная очередь); `transcription`
# сюда не входит — её слушает только `worker-transcriber`(-gpu), см. их
# комментарий ниже. Вынос очередей в отдельные реплики при масштабировании
# — `docs/deploy/scaling.md`.
worker:
build:
context: ../backend
dockerfile: Dockerfile
restart: unless-stopped
command: ["uv", "run", "celery", "-A", "workers.celery_app", "worker", "-B",
"-Q", "celery,summarize,notify", "--loglevel=info"]
env_file:
- ../.env
environment:
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml}
PYTHONPATH: /app
volumes:
- ../workers:/app/workers:ro
# plugins.yaml из корня репозитория (см. комментарий у сервиса backend)
- ../config:/app/config:ro
# tokenizer.json модели Qwen (профиль `llm`, скачан llm-model-init) —
# нужен QwenTokenCounter для подсчёта токенов при чанкинге транскрипта
# (backend/core/summarization/tokens.py). Без профиля `llm` том пуст —
# QwenTokenCounter уходит в фолбэк-эвристику len(text)//3.
- llm-models:/models/qwen:ro
depends_on:
redis:
condition: service_healthy
# Сервис worker использует тот же backend-образ (см. build выше), поэтому
# без переопределения он наследует HEALTHCHECK из backend/Dockerfile
# (curl /api/health), который здесь бессмысленен — у celery-контейнера
# нет HTTP-сервера на 8000. Проверяем воркер через `celery ... inspect
# ping`, как рекомендует документация Celery.
healthcheck:
test: ["CMD", "uv", "run", "celery", "-A", "workers.celery_app", "inspect", "ping", "--timeout", "5"]
interval: 15s
timeout: 10s
retries: 5
start_period: 20s
logging: *default-logging
# --- Профили transcribe/transcribe-gpu: том whisper-cache создаётся
# Docker root:root при первом использовании — случайно совпадает с uid
# backend-образа (в backend/Dockerfile НЕТ директивы USER, процессы внутри
# выполняются от root, uid 0), но фиксируем владельца явно, по образцу
# `recordings-init` — страховка от смены дефолтного uid образа/докера
# в будущем.
whisper-cache-init:
image: busybox:1.36
command: ["chown", "-R", "0:0", "/models/whisper"]
volumes:
- whisper-cache:/models/whisper
restart: "no"
profiles: ["transcribe", "transcribe-gpu"]
logging: *default-logging
# Разовая предзагрузка модели faster-whisper уровня AI (иначе первая
# транскрибация после старта воркера сама тянет веса из HuggingFace Hub —
# тот же дефект, что и с правами тома выше). `WHISPER_MODEL`
# пишет install.sh по выбранному пресету (3 → small, 4 → medium,
# 5 → large-v3, см. ADR-004); путь кэша — `/models/whisper/<модель>`,
# КОНТРАКТ с `backend/services/ai_tiers.py::WHISPER_MODELS_ROOT` (см.
# докстринг `deploy/whisper/download-model.py`) — используется и профилем
# `transcribe` (CPU, пресеты 3/4), и `transcribe-gpu` (пресет 5, max).
whisper-model-init:
build:
context: ../backend
dockerfile: Dockerfile
entrypoint: ["uv", "run", "python", "/download-model.py"]
environment:
WHISPER_MODEL: ${WHISPER_MODEL:-small}
WHISPER_MODELS_ROOT: /models/whisper
volumes:
- ./whisper/download-model.py:/download-model.py:ro
- whisper-cache:/models/whisper
restart: "no"
depends_on:
whisper-cache-init:
condition: service_completed_successfully
profiles: ["transcribe", "transcribe-gpu"]
logging: *default-logging
# --- Профиль transcribe: отдельный celery-воркер очереди `transcription`
# (faster-whisper, CPU — уровни AI `min`/`medium`, ADR-004: `medium`
# остаётся CPU-only в матрице `backend/services/ai_tiers.py`, GPU для него —
# ручная настройка администратора вне детекта). Изолирован от базового
# `worker`, потому что ctranslate2/faster-whisper несовместимы с
# prefork-пулом Celery (fork процесса после инициализации нативных
# библиотек небезопасен) — здесь `--pool=solo --concurrency=1`, модель —
# синглтон на процесс. Базовый `worker` очередь `transcription` не
# слушает (см. его command выше — явный `-Q` без `transcription`).
# Start with: docker compose --profile transcribe up -d
# (livekit из профиля `media` должен быть поднят отдельно/вместе).
worker-transcriber:
build:
context: ../backend
dockerfile: Dockerfile
restart: unless-stopped
# Фиксированное имя узла (`--hostname`) нужно, чтобы healthcheck ниже
# мог обратиться именно к этому воркеру: broker/redis общий с базовым
# `worker`, и `celery inspect ping` без `--destination` опросит ВЕСЬ
# кластер — упавший worker-transcriber остался бы "healthy", потому что
# ответил бы базовый worker.
command: ["uv", "run", "celery", "-A", "workers.celery_app", "worker",
"-Q", "transcription", "--pool=solo", "--concurrency=1",
"--hostname=worker-transcriber@localhost", "--loglevel=info"]
env_file:
- ../.env
environment:
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml}
RECORDINGS_DIR: ${RECORDINGS_DIR:-/recordings}
PYTHONPATH: /app
volumes:
- ../workers:/app/workers:ro
# plugins.yaml из корня репозитория (см. комментарий у сервиса backend)
- ../config:/app/config:ro
- recordings:/recordings:ro
- whisper-cache:/models/whisper
# Лимиты ресурсов CPU-bound транскрибации (faster-whisper на CPU).
# `cpus`/`mem_limit` — прямые атрибуты Compose (применяются и без
# swarm), в отличие от `deploy.resources`, который вне `docker stack
# deploy` игнорируется.
cpus: "4"
mem_limit: 6g
depends_on:
redis:
condition: service_healthy
whisper-model-init:
condition: service_completed_successfully
# HTTP-эндпоинта нет — пинг celery, но именно этого узла (см. --hostname
# в command выше), а не первого ответившего в общем кластере.
healthcheck:
test: ["CMD", "uv", "run", "celery", "-A", "workers.celery_app", "inspect", "ping", "--timeout", "5", "--destination", "worker-transcriber@localhost"]
interval: 15s
timeout: 10s
retries: 5
start_period: 20s
profiles: ["transcribe"]
logging: *default-logging
# --- Профиль transcribe-gpu: GPU-вариант транскрайбера, ТОЛЬКО уровень AI
# `max` (ADR-004, `backend/services/ai_tiers.py`: `medium` в матрице
# остаётся CPU-only, GPU для него — ручная настройка вне детекта, поэтому
# отдельного GPU-профиля для пресета 4 нет — см. комментарий у
# worker-transcriber выше). Собран из того же Dockerfile backend с
# extras-группой `gpu` (`nvidia-cublas-cu12`, `nvidia-cudnn-cu12==9.*` —
# объявлена в `backend/pyproject.toml`); действующий провайдер
# `faster_whisper_gpu` выбирается конфигом (TIERS["max"], не этим файлом).
# Взаимоисключаем c `worker-transcriber` через профили: одновременно оба
# профиля не поднимаются (install.sh выбирает ровно один по пресету).
worker-transcriber-gpu:
build:
context: ../backend
dockerfile: Dockerfile
args:
WITH_GPU_EXTRA: "true"
restart: unless-stopped
command: ["uv", "run", "celery", "-A", "workers.celery_app", "worker",
"-Q", "transcription", "--pool=solo", "--concurrency=1",
"--hostname=worker-transcriber-gpu@localhost", "--loglevel=info"]
env_file:
- ../.env
environment:
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml}
RECORDINGS_DIR: ${RECORDINGS_DIR:-/recordings}
PYTHONPATH: /app
# cuBLAS/cuDNN9, поставленные extras-группой `gpu` в venv образа (не
# системные библиотеки CUDA) — CTranslate2 находит их только через
# LD_LIBRARY_PATH, см. ADR-004 (faster-whisper README).
LD_LIBRARY_PATH: /app/.venv/lib/python3.12/site-packages/nvidia/cublas/lib:/app/.venv/lib/python3.12/site-packages/nvidia/cudnn/lib
volumes:
- ../workers:/app/workers:ro
- ../config:/app/config:ro
- recordings:/recordings:ro
- whisper-cache:/models/whisper
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
depends_on:
redis:
condition: service_healthy
whisper-model-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "uv", "run", "celery", "-A", "workers.celery_app", "inspect", "ping", "--timeout", "5", "--destination", "worker-transcriber-gpu@localhost"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
profiles: ["transcribe-gpu"]
logging: *default-logging
nginx:
# Собственный образ: nginx со вкомпилированной статикой фронтенд-SPA
# (frontend/Dockerfile, multi-stage: Vite-сборка → nginx). Раздаётся во
# ВСЕХ пресетах (сервис без profiles), поэтому http://localhost/ отдаёт
# рабочий фронт, а не заглушку. Конфиг реверс-прокси по-прежнему
# монтируется томом ниже — правки прокси не требуют пересборки фронта.
build:
context: ../frontend
dockerfile: Dockerfile
image: vidconf-nginx:latest
restart: unless-stopped
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
# Аватары: тот же volume, что у backend — nginx раздаёт файлы
# напрямую (`location /media/`, alias), в обход backend-процесса.
- media:/media:ro
ports:
- "80:80"
depends_on:
backend:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:80/"]
interval: 10s
timeout: 5s
retries: 5
logging: *default-logging
# --- Media profile: LiveKit SFU + TURN. Disabled by default. ---
# Start with: docker compose --profile media up -d
livekit:
image: livekit/livekit-server:latest
restart: unless-stopped
command: --config /etc/livekit.yaml
volumes:
- ./livekit/livekit.yaml:/etc/livekit.yaml:ro
environment:
# Формат LIVEKIT_KEYS СТРОГО "key: secret" (двоеточие + пробел),
# иначе LiveKit падает в crash-loop с "Could not parse keys". Значение
# обязательно в кавычках: YAML запрещает ": " внутри неквотированного
# plain-скаляра.
LIVEKIT_KEYS: "${LIVEKIT_API_KEY:-devkey}: ${LIVEKIT_API_SECRET:-change-me-livekit-secret}"
ports:
- "7880:7880" # HTTP/WebSocket signaling
- "7881:7881" # RTC TCP fallback
# Узкий диапазон для dev на macOS: широкий (50000-60000) почти всегда
# конфликтует с занятыми UDP-портами хоста и тормозит Docker Desktop.
# 54000+ выбран после конфликтов: нижние диапазоны (50000+, 52000+)
# занимают Steam/системные процессы macOS и эфемерные QUIC-соединения.
- "54000-54100:54000-54100/udp" # WebRTC media (ICE)
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:7880/"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
profiles: ["media"]
logging: *default-logging
coturn:
image: coturn/coturn:latest
restart: unless-stopped
command: -c /etc/coturn/turnserver.conf
volumes:
- ./coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro
environment:
TURN_REALM: ${TURN_REALM:-vidconf.local}
TURN_STATIC_AUTH_SECRET: ${TURN_STATIC_AUTH_SECRET:-change-me-turn-secret}
network_mode: host
# Образ coturn/coturn — минимальный (debian-slim), в нём нет pgrep/ps/nc/
# curl/wget, поэтому проверка процесса по имени не работает
# ("pgrep: not found" → healthcheck всегда unhealthy). Вместо этого
# опрашиваем сам TURN-сервер STUN-запросом через штатную утилиту
# turnutils_stunclient, которая есть в образе.
healthcheck:
test: ["CMD-SHELL", "turnutils_stunclient -p 3478 127.0.0.1 || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
profiles: ["media"]
logging: *default-logging
# --- Профиль transcribe: LiveKit Egress — запись per-track аудио для
# последующей транскрибации (см. ADR-002). Egress запускает обработчик каждой записи как отдельный
# процесс внутри своего же контейнера (не Docker-in-Docker — проверено
# по официальной документации livekit/egress), поэтому доступ к
# /var/run/docker.sock не требуется. Записи и без транскрибации никому
# не нужны — profiles совпадает с worker-transcriber; livekit (профиль
# `media`) при этом должен быть поднят отдельно/вместе.
# Start with: docker compose --profile media --profile transcribe up -d
# Именованный том recordings при первом создании принадлежит root (0:0,
# drwxr-xr-x), а процесс egress работает от uid 1001 (gid 0) и не может
# создавать в нём каталоги сеансов. Однократный init-контейнер отдаёт том
# владельцу-egress до старта записи (иначе Track Egress падает с
# "Local upload failed: mkdir ... permission denied").
recordings-init:
image: busybox:1.36
command: ["chown", "-R", "1001:0", "/recordings"]
volumes:
- recordings:/recordings
restart: "no"
profiles: ["transcribe"]
logging: *default-logging
egress:
image: livekit/egress:latest
restart: unless-stopped
volumes:
- ./egress/egress.yaml:/etc/egress.yaml:ro
- recordings:/recordings
environment:
EGRESS_CONFIG_FILE: /etc/egress.yaml
# api_key/api_secret/ws_url — обязательные переменные окружения
# (см. deploy/egress/egress.yaml); ws_url — внутренний адрес сервиса
# livekit в docker-сети, НЕ LIVEKIT_PUBLIC_URL для браузера.
LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey}
LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-change-me-livekit-secret}
LIVEKIT_WS_URL: ${LIVEKIT_WS_URL:-ws://livekit:7880}
depends_on:
# required: false — livekit живёт в профиле `media`, а не
# `transcribe`; без этого `docker compose --profile transcribe up`
# (без media) падает с ошибкой "service livekit ... is disabled".
# На практике egress без livekit бесполезен — профили запускают
# вместе (см. комментарий выше), но это не должно ломать валидацию
# конфигурации, если кто-то поднимает профили по отдельности.
livekit:
condition: service_healthy
required: false
redis:
condition: service_healthy
recordings-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "curl", "-sf", "http://127.0.0.1:8081/healthz"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
profiles: ["transcribe"]
logging: *default-logging
# --- Профили llm/llm-gpu: том llm-models создаётся Docker root:root при
# первом использовании — по тем же соображениям, что и whisper-cache-init
# выше (backend-образ и curlimages/curl оба в итоге пишут
# от root — см. `user: "0:0"` у llm-model-init ниже), явный init-контейнер
# фиксирует владельца, по образцу `recordings-init`.
llm-models-init:
image: busybox:1.36
command: ["chown", "-R", "0:0", "/models/qwen"]
volumes:
- llm-models:/models/qwen
restart: "no"
profiles: ["llm", "llm-gpu"]
logging: *default-logging
# --- Профили llm/llm-gpu: локальный LLM-сервер для плагина суммаризации
# QwenLocal (см. ADR-004). Один и тот же
# init-контейнер обслуживает оба профиля — уровень AI (`LLM_MODEL_*`)
# определяет install.sh при выборе пресета 3/4/5, а не профиль CPU/GPU.
# Start with: docker compose --profile llm up -d
#
# Разовое скачивание GGUF-модели уровня AI и tokenizer.json в общий volume
# `llm-models`. Идемпотентен — при повторном запуске (файлы уже в volume)
# ничего не перекачивает, см. deploy/llm/download-model.sh.
llm-model-init:
image: curlimages/curl:8.10.1
entrypoint: ["/bin/sh", "/download-model.sh"]
environment:
LLM_MODEL_FILE: ${LLM_MODEL_FILE:-qwen3.5-4b-instruct-q4_k_m.gguf}
LLM_MODEL_URL: ${LLM_MODEL_URL:-https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf}
LLM_MODEL_MIN_SIZE: ${LLM_MODEL_MIN_SIZE:-2000000000}
LLM_TOKENIZER_FILE: ${LLM_TOKENIZER_FILE:-qwen3.5-4b-instruct.tokenizer.json}
LLM_TOKENIZER_URL: ${LLM_TOKENIZER_URL:-https://huggingface.co/Qwen/Qwen3.5-4B/resolve/main/tokenizer.json}
# Образ curlimages/curl по умолчанию работает от непривилегированного
# curl_user (uid 100) — записать .part-файл в /models/qwen не получится
# без явного root (том фиксирован под 0:0 в llm-models-init выше).
user: "0:0"
volumes:
- ./llm/download-model.sh:/download-model.sh:ro
- llm-models:/models/qwen
restart: "no"
depends_on:
llm-models-init:
condition: service_completed_successfully
profiles: ["llm", "llm-gpu"]
logging: *default-logging
# LLM-сервер (CPU) на базе официального образа llama.cpp (OpenAI-
# совместимый /v1/chat/completions, используется QwenLocal через
# OpenAICompatClient). Конфигурация — исключительно через переменные
# окружения LLAMA_ARG_* (штатный механизм образа, см.
# tools/server/README.md проекта llama.cpp). Тег закреплён по номеру
# сборки (не плавающий `:server`) — см. ADR-004, «Сводка рисков»,
# п.1; актуальный тег проверен по реестру ghcr.io.
llm:
image: ghcr.io/ggml-org/llama.cpp:server-b10068
restart: unless-stopped
environment:
LLAMA_ARG_MODEL: /models/qwen/${LLM_MODEL_FILE:-qwen3.5-4b-instruct-q4_k_m.gguf}
# 8k токенов на чанк (верхняя граница чанкера) + промпт + до 1536
# токенов вывода (max-уровень reduce), с запасом на служебные токены.
LLAMA_ARG_CTX_SIZE: 16384
LLAMA_ARG_HOST: 0.0.0.0
LLAMA_ARG_PORT: 8080
# Отключение thinking-режима (ADR-004: семейство Qwen3.5 Small — по
# умолчанию выключен, но фиксируем явно). `--chat-template-kwargs
# '{"enable_thinking":false}'`, упомянутый в ADR-004, на актуальной
# версии llama.cpp помечен деприкейтед в пользу `--reasoning off`
# (проверено по common/arg.cpp: одинаковый эффект —
# `default_template_kwargs["enable_thinking"]="false"`, но без
# предупреждения в логе на каждый запуск).
LLAMA_ARG_REASONING: "off"
# Экспозиция /metrics для Prometheus (job `llm`, deploy/monitoring/prometheus.yml).
LLAMA_ARG_ENDPOINT_METRICS: 1
volumes:
- llm-models:/models/qwen:ro
ports:
- "8080:8080"
depends_on:
llm-model-init:
condition: service_completed_successfully
# /health отдаёт 503 ("Loading model"), пока модель грузится в память,
# и 200 ("status": "ok"), когда сервер готов принимать запросы —
# штатный эндпоинт образа llama.cpp (curl есть в базовом слое образа,
# см. .devops/cpu.Dockerfile проекта llama.cpp — используется как в
# фирменном HEALTHCHECK образа).
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/health"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
profiles: ["llm"]
logging: *default-logging
# --- Профиль llm-gpu: GPU-вариант LLM-сервера, ТОЛЬКО уровень AI `max`
# (ADR-004: единственный уровень с `base_url: http://llm-gpu:8080/v1` в
# `backend/services/ai_tiers.py`). Официальный CUDA-образ llama.cpp
# (CUDA 12); тег закреплён по номеру сборки, синхронно с `llm` выше.
llm-gpu:
image: ghcr.io/ggml-org/llama.cpp:server-cuda-b10068
restart: unless-stopped
environment:
LLAMA_ARG_MODEL: /models/qwen/${LLM_MODEL_FILE:-qwen3.5-35b-a3b-instruct-q4_k_m.gguf}
LLAMA_ARG_CTX_SIZE: 16384
LLAMA_ARG_HOST: 0.0.0.0
LLAMA_ARG_PORT: 8080
# Полный offload в VRAM (ADR-004: max — обязательный GPU ≥16 ГБ,
# рекомендовано 24 ГБ; 999 — общепринятое в доках llama.cpp значение
# «офлоадить все слои», сервер сам ограничивает реальным числом слоёв
# модели, если их меньше).
LLAMA_ARG_N_GPU_LAYERS: 999
LLAMA_ARG_REASONING: "off"
LLAMA_ARG_ENDPOINT_METRICS: 1
volumes:
- llm-models:/models/qwen:ro
ports:
- "8081:8080"
# Алиас `llm` в сети compose (в дополнение к штатному `llm-gpu`) —
# `llm` и `llm-gpu` взаимоисключающи по профилю (install.sh включает
# ровно один), поэтому Prometheus (deploy/monitoring/prometheus.yml,
# job `llm`) всегда скрейпит один и тот же адрес `llm:8080` независимо
# от того, какой из двух реально поднят — без дублирования job'ов и
# вечно "красного" targets для неиспользуемого профиля.
networks:
default:
aliases:
- llm
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
depends_on:
llm-model-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/health"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
profiles: ["llm-gpu"]
logging: *default-logging
# --- Профиль monitoring: Prometheus + Grafana + exporter'ы.
# Дашборд «Пайплайны пост-обработки» — deploy/monitoring/grafana/.
# Start with: docker compose --profile monitoring up -d
# (можно вместе с любыми другими профилями — независимый набор сервисов).
prometheus:
# Тег закреплён по версии (не `:latest`) — та же причина, что и у llm-образов.
image: prom/prometheus:v3.13.1
restart: unless-stopped
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./monitoring/alerts.yml:/etc/prometheus/alerts.yml:ro
- prometheus_data:/prometheus
ports:
- "9090:9090"
healthcheck:
test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9090/-/healthy || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
profiles: ["monitoring"]
logging: *default-logging
postgres-exporter:
image: quay.io/prometheuscommunity/postgres-exporter:v0.20.1
restart: unless-stopped
environment:
DATA_SOURCE_NAME: "postgresql://${POSTGRES_USER:-vidconf}:${POSTGRES_PASSWORD:-vidconf}@postgres:5432/${POSTGRES_DB:-vidconf}?sslmode=disable"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9187/metrics >/dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
profiles: ["monitoring"]
logging: *default-logging
redis-exporter:
image: oliver006/redis_exporter:v1.87.0-alpine
restart: unless-stopped
environment:
REDIS_ADDR: "redis://redis:6379"
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:9121/metrics >/dev/null || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
profiles: ["monitoring"]
logging: *default-logging
grafana:
image: grafana/grafana:13.1.0
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
# Дефолт только для dev — install.sh генерирует секрет при первой
# установке (секреты только в .env).
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-change-me-grafana}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
- grafana_data:/var/lib/grafana
ports:
- "3001:3000"
depends_on:
prometheus:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:3000/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 15s
profiles: ["monitoring"]
logging: *default-logging
volumes:
postgres_data:
redis_data:
# Общий том между egress (пишет) и worker-transcriber (читает :ro):
# per-track .ogg-записи (profile transcribe).
recordings:
# Кэш весов faster-whisper — переживает пересоздание контейнера. Каждый
# уровень AI хранит модель в подкаталоге `<volume>/<модель>` (`small`/
# `medium`/`large-v3`) — КОНТРАКТ с `backend/services/ai_tiers.py`
# (WHISPER_MODELS_ROOT) и детектом доступности уровня, см.
# `deploy/whisper/download-model.py`.
whisper-cache:
# GGUF-модель Qwen3.5 уровня AI + tokenizer.json (профили `llm`/`llm-gpu`,
# скачивает llm-model-init) — общий том между `llm`/`llm-gpu` (инференс) и
# `worker` (подсчёт токенов чанкером через QwenTokenCounter). Имена файлов
# — КОНТРАКТ с `backend/services/ai_tiers.py` (QWEN_MODELS_ROOT).
llm-models:
# Загруженные пользователями файлы (аватары) — общий том между
# backend (запись при загрузке) и nginx (раздача статики, `location /media/`).
media:
# Метрики Prometheus (профиль `monitoring`) — переживают пересоздание контейнера.
prometheus_data:
# Дашборды/настройки Grafana (профиль `monitoring`) — переживают пересоздание.
grafana_data: