451 lines
24 KiB
Bash
Executable File
451 lines
24 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Инсталлятор VidConf.
|
||
#
|
||
# Опросник из 5 пресетов поставки:
|
||
# 1) MVP-ядро (лобби, конференции, календарь, закреплённые, гости)
|
||
# 2) 1 + чат
|
||
# 3) 2 + AI-уровень «мин» (CPU, faster-whisper small + Qwen3.5-4B)
|
||
# 4) 2 + AI-уровень «средний» (CPU, faster-whisper medium + Qwen3.5-9B)
|
||
# 5) 2 + AI-уровень «макс» (GPU ОБЯЗАТЕЛЕН, faster-whisper large-v3 + Qwen3.5-35B-A3B)
|
||
# Модели/железо — ADR-004 (docs/architecture/adr/004-ai-tier-matrix.md),
|
||
# единственный источник истины. Запись конференций сюда НЕ входит — она
|
||
# появится в одной из следующих версий.
|
||
#
|
||
# Идемпотентность: повторный запуск = апгрейд/даунгрейд пресета на месте.
|
||
# Секреты и пользовательские правки .env НЕ теряются — значения пишутся
|
||
# точечно (set_env_var ниже), а не перезаписью всего файла. Seed-скрипт
|
||
# backend (scripts/seed.py) и бутстрап instance_settings (backend, lifespan)
|
||
# идемпотентны сами по себе — существующие настройки не перетираются.
|
||
#
|
||
# Использование:
|
||
# ./install.sh # интерактивный опросник
|
||
# ./install.sh --preset 3 # неинтерактивно, пресет 3
|
||
# ./install.sh --preset 3 --yes # без подтверждений (скрипты/CI)
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
cd "$SCRIPT_DIR"
|
||
|
||
COMPOSE_FILE="deploy/docker-compose.yml"
|
||
ENV_FILE="$SCRIPT_DIR/.env"
|
||
ENV_EXAMPLE="$SCRIPT_DIR/.env.example"
|
||
VERSION_FILE="$SCRIPT_DIR/VERSION"
|
||
|
||
PRESET=""
|
||
ASSUME_YES=0
|
||
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--preset)
|
||
PRESET="$2"
|
||
shift 2
|
||
;;
|
||
--yes|-y)
|
||
ASSUME_YES=1
|
||
shift
|
||
;;
|
||
-h|--help)
|
||
grep -E '^#( |$)' "$0" | sed 's/^#//'
|
||
exit 0
|
||
;;
|
||
*)
|
||
echo "Неизвестный аргумент: $1" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Утилиты работы с .env: точечное обновление ключа (не трогает остальные
|
||
# строки/комментарии/порядок) — идемпотентность и сохранность пользовательских
|
||
# правок при повторном запуске.
|
||
# ---------------------------------------------------------------------------
|
||
|
||
FRESH_ENV=0
|
||
|
||
# `.env.example` в git хранит ПЛЕЙСХОЛДЕРЫ секретов (`change-me...`,
|
||
# `POSTGRES_PASSWORD=vidconf` и т.п.) — не пустые строки. Если после
|
||
# `cp .env.example .env` оставить их как есть, `ensure_secret` ниже (генерация
|
||
# «только при отсутствии») их не тронет, и на проде осядут секреты из
|
||
# публичного репозитория. Поэтому при ПЕРВОМ создании .env явно очищаем
|
||
# управляемые секреты перед вызовом ensure_secret — на повторных запусках
|
||
# (файл уже существует) эта функция не трогает ничего, ensure_secret
|
||
# сохраняет и сгенерированные, и вручную заданные пользователем значения.
|
||
ensure_env_file() {
|
||
if [ ! -f "$ENV_FILE" ]; then
|
||
echo "[install] .env не найден — создаю из .env.example"
|
||
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
||
FRESH_ENV=1
|
||
fi
|
||
}
|
||
|
||
get_env_var() {
|
||
key="$1"
|
||
[ -f "$ENV_FILE" ] || { echo ""; return 0; }
|
||
grep -E "^${key}=" "$ENV_FILE" 2>/dev/null | tail -1 | cut -d= -f2- || true
|
||
}
|
||
|
||
# Установить/заменить KEY=VALUE в .env; добавить строку, если ключа ещё нет.
|
||
# `sed -i.bak ... && rm -f *.bak` — переносимо между GNU sed (Linux) и BSD
|
||
# sed (macOS): оба принимают суффикс бэкапа сразу после -i, поведение без
|
||
# суффикса на BSD sed отличается (требует отдельного аргумента) — этим и
|
||
# отличаются друг от друга, суффикс убирает разницу.
|
||
set_env_var() {
|
||
key="$1"
|
||
value="$2"
|
||
esc_value=$(printf '%s' "$value" | sed -e 's/[&|\\]/\\&/g')
|
||
if grep -qE "^${key}=" "$ENV_FILE"; then
|
||
sed -i.bak "s|^${key}=.*|${key}=${esc_value}|" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
|
||
else
|
||
printf '%s=%s\n' "$key" "$value" >> "$ENV_FILE"
|
||
fi
|
||
}
|
||
|
||
# Установить секрет ТОЛЬКО если он ещё не задан (не перезаписывать
|
||
# сгенерированные ранее/вручную заданные пользователем значения).
|
||
ensure_secret() {
|
||
key="$1"
|
||
bytes="${2:-32}"
|
||
current="$(get_env_var "$key")"
|
||
if [ -z "$current" ]; then
|
||
generated="$(openssl rand -hex "$bytes" 2>/dev/null || head -c "$bytes" /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||
set_env_var "$key" "$generated"
|
||
echo "[install] ${key}: сгенерирован новый секрет"
|
||
fi
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Автодетект железа (ADR-004, таблица требований).
|
||
# ---------------------------------------------------------------------------
|
||
|
||
detect_cpus() {
|
||
if command -v nproc >/dev/null 2>&1; then
|
||
nproc
|
||
elif command -v sysctl >/dev/null 2>&1; then
|
||
sysctl -n hw.ncpu 2>/dev/null || echo 0
|
||
else
|
||
echo 0
|
||
fi
|
||
}
|
||
|
||
detect_ram_mb() {
|
||
if command -v free >/dev/null 2>&1; then
|
||
free -m | awk '/^Mem:/{print $2}'
|
||
elif command -v sysctl >/dev/null 2>&1; then
|
||
bytes=$(sysctl -n hw.memsize 2>/dev/null || echo 0)
|
||
echo $((bytes / 1024 / 1024))
|
||
else
|
||
echo 0
|
||
fi
|
||
}
|
||
|
||
# Печатает "имя_gpu|vram_mb" через `|` (имя может содержать пробелы) или
|
||
# ничего, если nvidia-smi недоступен/GPU не найден.
|
||
detect_gpu() {
|
||
if command -v nvidia-smi >/dev/null 2>&1; then
|
||
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits 2>/dev/null \
|
||
| head -1 \
|
||
| awk -F', *' '{print $1"|"$2}'
|
||
fi
|
||
}
|
||
|
||
HW_CPUS="$(detect_cpus)"
|
||
HW_RAM_MB="$(detect_ram_mb)"
|
||
HW_GPU_LINE="$(detect_gpu || true)"
|
||
HW_GPU_NAME="${HW_GPU_LINE%%|*}"
|
||
HW_VRAM_MB="${HW_GPU_LINE##*|}"
|
||
if [ "$HW_GPU_LINE" = "" ] || [ "$HW_GPU_NAME" = "$HW_VRAM_MB" ]; then
|
||
HW_GPU_NAME=""
|
||
HW_VRAM_MB=""
|
||
fi
|
||
|
||
echo "[install] Обнаружено железо: CPU=${HW_CPUS} vCPU, RAM=${HW_RAM_MB} МБ, GPU=${HW_GPU_NAME:-нет}${HW_VRAM_MB:+, VRAM=${HW_VRAM_MB} МБ}"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Рекомендация пресета по обнаруженному железу (таблица ADR-004).
|
||
# ---------------------------------------------------------------------------
|
||
|
||
recommend_preset() {
|
||
if [ -n "$HW_VRAM_MB" ] && [ "$HW_VRAM_MB" -ge 16000 ] 2>/dev/null \
|
||
&& [ "$HW_RAM_MB" -ge 65536 ] 2>/dev/null && [ "$HW_CPUS" -ge 16 ] 2>/dev/null; then
|
||
echo 5
|
||
elif [ "$HW_RAM_MB" -ge 32768 ] 2>/dev/null && [ "$HW_CPUS" -ge 12 ] 2>/dev/null; then
|
||
echo 4
|
||
elif [ "$HW_RAM_MB" -ge 16384 ] 2>/dev/null && [ "$HW_CPUS" -ge 8 ] 2>/dev/null; then
|
||
echo 3
|
||
else
|
||
echo 2
|
||
fi
|
||
}
|
||
|
||
RECOMMENDED_PRESET="$(recommend_preset)"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Опросник (интерактивный, если --preset не передан).
|
||
# ---------------------------------------------------------------------------
|
||
|
||
print_menu() {
|
||
cat <<EOF
|
||
|
||
Выберите пресет поставки VidConf:
|
||
1) MVP-ядро — лобби, конференции, календарь, закреплённые, гости
|
||
2) 1 + чат — то же + групповой чат конференции
|
||
3) 2 + AI «мин» — CPU: faster-whisper small + Qwen3.5-4B (8 vCPU / 16 ГБ RAM / 100 ГБ)
|
||
4) 2 + AI «средний» — CPU: faster-whisper medium + Qwen3.5-9B (12-16 vCPU / 32 ГБ RAM / 150 ГБ)
|
||
5) 2 + AI «макс» — GPU ОБЯЗАТЕЛЕН: faster-whisper large-v3 + Qwen3.5-35B-A3B (16+ vCPU / 64 ГБ RAM / GPU NVIDIA ≥16 ГБ VRAM / 250 ГБ)
|
||
|
||
Рекомендация по обнаруженному железу: пресет ${RECOMMENDED_PRESET}.
|
||
Подробности требований — docs/architecture/adr/004-ai-tier-matrix.md.
|
||
EOF
|
||
}
|
||
|
||
if [ -z "$PRESET" ]; then
|
||
print_menu
|
||
printf "Номер пресета [%s]: " "$RECOMMENDED_PRESET"
|
||
read -r answer
|
||
PRESET="${answer:-$RECOMMENDED_PRESET}"
|
||
fi
|
||
|
||
case "$PRESET" in
|
||
1|2|3|4|5) ;;
|
||
*)
|
||
echo "Пресет должен быть числом от 1 до 5 (получено: '${PRESET}')" >&2
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# Пресет 5 (max) требует GPU ОБЯЗАТЕЛЬНО (ADR-004) — если детект его не
|
||
# нашёл, явно предупреждаем и просим подтверждение (можно ставить заранее,
|
||
# до подключения GPU-хоста, но AI-контейнеры не поднимутся здоровыми).
|
||
if [ "$PRESET" = "5" ] && { [ -z "$HW_VRAM_MB" ] || [ "$HW_VRAM_MB" -lt 16000 ]; }; then
|
||
echo "[install] ВНИМАНИЕ: пресет 5 требует NVIDIA GPU ≥16 ГБ VRAM — не обнаружен (или недостаточно VRAM)." >&2
|
||
if [ "$ASSUME_YES" != "1" ]; then
|
||
printf "Продолжить всё равно? [y/N]: "
|
||
read -r confirm
|
||
case "$confirm" in
|
||
y|Y|yes|Yes) ;;
|
||
*) echo "Отменено."; exit 1 ;;
|
||
esac
|
||
fi
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Заполнение .env: секреты (только при отсутствии), железо, версия, модели,
|
||
# профили compose — точечно, см. set_env_var.
|
||
# ---------------------------------------------------------------------------
|
||
|
||
ensure_env_file
|
||
|
||
if [ "$FRESH_ENV" = "1" ]; then
|
||
for key in JWT_SECRET POSTGRES_PASSWORD TURN_STATIC_AUTH_SECRET \
|
||
LIVEKIT_API_SECRET GRAFANA_ADMIN_PASSWORD SEED_ADMIN_PASSWORD; do
|
||
set_env_var "$key" ""
|
||
done
|
||
fi
|
||
|
||
ensure_secret JWT_SECRET 32
|
||
ensure_secret POSTGRES_PASSWORD 16
|
||
# DATABASE_URL хранит пароль ВНУТРИ строки подключения — пересобираем её из
|
||
# POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB на каждом запуске (не только
|
||
# при первом), иначе после регенерации POSTGRES_PASSWORD backend/worker
|
||
# продолжили бы стучаться со старым паролем из .env.example и не
|
||
# подключились бы к БД. Хост — `postgres` (имя сервиса в docker-сети
|
||
# compose), не `localhost` (тот годится только для запуска backend на хосте
|
||
# вне контейнера, см. docs/deploy/dev-setup.md).
|
||
set_env_var DATABASE_URL "postgresql+asyncpg://$(get_env_var POSTGRES_USER):$(get_env_var POSTGRES_PASSWORD)@postgres:5432/$(get_env_var POSTGRES_DB)"
|
||
ensure_secret TURN_STATIC_AUTH_SECRET 24
|
||
ensure_secret LIVEKIT_API_SECRET 32
|
||
ensure_secret GRAFANA_ADMIN_PASSWORD 16
|
||
# SEED_ADMIN_PASSWORD — единственный секрет с человекочитаемым дефолтом
|
||
# (сообщается пользователю в конце) вместо случайной генерации: это учётные
|
||
# данные, которыми оператор будет логиниться вручную сразу после установки.
|
||
if [ -z "$(get_env_var SEED_ADMIN_PASSWORD)" ]; then
|
||
generated_admin_pw="$(openssl rand -hex 8 2>/dev/null || head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n')"
|
||
set_env_var SEED_ADMIN_PASSWORD "$generated_admin_pw"
|
||
fi
|
||
|
||
set_env_var VIDCONF_VERSION "$(cat "$VERSION_FILE")"
|
||
set_env_var HW_CPUS "$HW_CPUS"
|
||
set_env_var HW_RAM_MB "$HW_RAM_MB"
|
||
set_env_var HW_GPU_NAME "$HW_GPU_NAME"
|
||
set_env_var HW_VRAM_MB "$HW_VRAM_MB"
|
||
|
||
# Профили compose и модели по пресету. GPU-профили — ТОЛЬКО для пресета 5
|
||
# (max): в текущей матрице уровней (backend/services/ai_tiers.py, ADR-004)
|
||
# уровень `medium` фиксированно CPU-only (GPU для него — ручная настройка
|
||
# администратора вне детекта), поэтому отдельного GPU-варианта для пресета 4
|
||
# нет — не путать с прозой ADR-004 про «опциональный GPU для medium».
|
||
#
|
||
# BOOTSTRAP_CHAT_ENABLED/BOOTSTRAP_TRANSCRIPTION_ENABLED/BOOTSTRAP_AI_LEVEL:
|
||
# без них
|
||
# бутстрап настроек инстанса (backend, lifespan) брал ВСЁ enabled: true из
|
||
# config/plugins.yaml независимо от пресета, и на пресетах 1/2 (без AI)
|
||
# сеанс конференции навсегда зависал в очереди transcribe, которую некому
|
||
# обслуживать. Значения читает backend при первом бутстрапе И скрипт
|
||
# scripts.apply_preset_settings (принудительное применение, см. ниже).
|
||
case "$PRESET" in
|
||
1)
|
||
set_env_var COMPOSE_PROFILES "media"
|
||
set_env_var BOOTSTRAP_CHAT_ENABLED "false"
|
||
set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "false"
|
||
set_env_var BOOTSTRAP_AI_LEVEL "min"
|
||
;;
|
||
2)
|
||
set_env_var COMPOSE_PROFILES "media"
|
||
set_env_var BOOTSTRAP_CHAT_ENABLED "true"
|
||
set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "false"
|
||
set_env_var BOOTSTRAP_AI_LEVEL "min"
|
||
;;
|
||
3)
|
||
set_env_var COMPOSE_PROFILES "media,transcribe,llm"
|
||
set_env_var WHISPER_MODEL "small"
|
||
set_env_var LLM_MODEL_FILE "qwen3.5-4b-instruct-q4_k_m.gguf"
|
||
set_env_var LLM_MODEL_URL "https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf"
|
||
set_env_var LLM_MODEL_MIN_SIZE "2000000000"
|
||
set_env_var LLM_TOKENIZER_FILE "qwen3.5-4b-instruct.tokenizer.json"
|
||
set_env_var LLM_TOKENIZER_URL "https://huggingface.co/Qwen/Qwen3.5-4B/resolve/main/tokenizer.json"
|
||
set_env_var BOOTSTRAP_CHAT_ENABLED "true"
|
||
set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "true"
|
||
set_env_var BOOTSTRAP_AI_LEVEL "min"
|
||
;;
|
||
4)
|
||
set_env_var COMPOSE_PROFILES "media,transcribe,llm"
|
||
set_env_var WHISPER_MODEL "medium"
|
||
set_env_var LLM_MODEL_FILE "qwen3.5-9b-instruct-q4_k_m.gguf"
|
||
set_env_var LLM_MODEL_URL "https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf"
|
||
set_env_var LLM_MODEL_MIN_SIZE "5000000000"
|
||
set_env_var LLM_TOKENIZER_FILE "qwen3.5-9b-instruct.tokenizer.json"
|
||
set_env_var LLM_TOKENIZER_URL "https://huggingface.co/Qwen/Qwen3.5-9B/resolve/main/tokenizer.json"
|
||
set_env_var BOOTSTRAP_CHAT_ENABLED "true"
|
||
set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "true"
|
||
set_env_var BOOTSTRAP_AI_LEVEL "medium"
|
||
;;
|
||
5)
|
||
set_env_var COMPOSE_PROFILES "media,transcribe-gpu,llm-gpu"
|
||
set_env_var WHISPER_MODEL "large-v3"
|
||
set_env_var LLM_MODEL_FILE "qwen3.5-35b-a3b-instruct-q4_k_m.gguf"
|
||
set_env_var LLM_MODEL_URL "https://huggingface.co/unsloth/Qwen3.5-35B-A3B-GGUF/resolve/main/Qwen3.5-35B-A3B-Q4_K_M.gguf"
|
||
set_env_var LLM_MODEL_MIN_SIZE "20000000000"
|
||
set_env_var LLM_TOKENIZER_FILE "qwen3.5-35b-a3b-instruct.tokenizer.json"
|
||
set_env_var LLM_TOKENIZER_URL "https://huggingface.co/Qwen/Qwen3.5-35B-A3B/resolve/main/tokenizer.json"
|
||
set_env_var BOOTSTRAP_CHAT_ENABLED "true"
|
||
set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "true"
|
||
set_env_var BOOTSTRAP_AI_LEVEL "max"
|
||
;;
|
||
esac
|
||
|
||
echo "[install] .env обновлён (пресет ${PRESET}, COMPOSE_PROFILES=$(get_env_var COMPOSE_PROFILES))"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Подъём стека и миграции/seed.
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# `COMPOSE_PROFILES` в .env НЕ подхватывается автоматически командой
|
||
# `docker compose ... up` в этой версии compose (проверено на v5.3.1) —
|
||
# нужен явный повторяемый флаг `--profile`, иначе профильные сервисы
|
||
# (livekit/coturn для media, транскрибация/LLM для остальных пресетов)
|
||
# молча не поднимаются, хотя .env выглядит корректно заполненным.
|
||
PROFILE_ARGS=()
|
||
IFS=',' read -r -a _profiles <<< "$(get_env_var COMPOSE_PROFILES)"
|
||
for _p in "${_profiles[@]}"; do
|
||
[ -n "$_p" ] && PROFILE_ARGS+=(--profile "$_p")
|
||
done
|
||
|
||
echo "[install] docker compose pull (лучше сразу, но необязательный шаг — образы, которых нет в реестре под текущим тегом, пропускаются)"
|
||
docker compose -f "$COMPOSE_FILE" "${PROFILE_ARGS[@]}" pull --ignore-buildable 2>/dev/null || true
|
||
|
||
echo "[install] docker compose build"
|
||
docker compose -f "$COMPOSE_FILE" "${PROFILE_ARGS[@]}" build
|
||
|
||
echo "[install] Поднимаю postgres/redis (--wait) — нужны для миграций до старта backend"
|
||
docker compose -f "$COMPOSE_FILE" up -d --wait postgres redis
|
||
|
||
# backend.lifespan бутстрапит instance_settings при КАЖДОМ старте приложения,
|
||
# поэтому миграции обязаны применяться ДО первого запуска backend/worker —
|
||
# иначе на чистой БД backend падает на "relation instance_settings does not
|
||
# exist" и healthcheck (--wait) никогда не проходит. `compose run` запускает
|
||
# одноразовый контейнер с нужной командой, не поднимая uvicorn/lifespan.
|
||
echo "[install] Применяю миграции Alembic и seed (админ/справочники) — до старта backend"
|
||
docker compose -f "$COMPOSE_FILE" run --rm backend uv run alembic upgrade head
|
||
docker compose -f "$COMPOSE_FILE" run --rm backend uv run python -m scripts.seed
|
||
|
||
echo "[install] docker compose up -d --wait (backend/worker/nginx и остальные сервисы профиля)"
|
||
# Первый старт backend/worker включает `uv run` (синхронизация окружения +
|
||
# компиляция байткода) — на слабой/загруженной машине healthcheck может не
|
||
# успеть пройти за отведённые retries, и `--wait` вернёт "container is
|
||
# unhealthy", хотя сервис через несколько секунд становится healthy. Команда
|
||
# идемпотентна, поэтому повторяем её несколько раз: повтор лишь дожидается
|
||
# уже стартующих контейнеров, ничего не пересоздавая.
|
||
UP_ATTEMPTS=3
|
||
for attempt in $(seq 1 "$UP_ATTEMPTS"); do
|
||
if docker compose -f "$COMPOSE_FILE" "${PROFILE_ARGS[@]}" up -d --wait; then
|
||
break
|
||
fi
|
||
if [ "$attempt" -eq "$UP_ATTEMPTS" ]; then
|
||
echo "[install] ОШИБКА: сервисы не стали healthy за $UP_ATTEMPTS попыток." >&2
|
||
echo "[install] Проверьте логи: docker compose -f $COMPOSE_FILE logs backend worker" >&2
|
||
exit 1
|
||
fi
|
||
echo "[install] Не все сервисы healthy (попытка $attempt/$UP_ATTEMPTS) — жду 10с и повторяю..."
|
||
sleep 10
|
||
done
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Настройки модулей инстанса (чат/AI-уровень) под пресет — только на живой
|
||
# установке (fix/installer-preset-modules): при СВЕЖЕМ .env (FRESH_ENV=1)
|
||
# они уже применятся сами при бутстрапе backend (lifespan читает
|
||
# BOOTSTRAP_* из .env), повторно накатывать не нужно. При ПОВТОРНОМ запуске
|
||
# .env уже существовал ДО этого запуска — настройки уже в БД и могли быть
|
||
# вручную изменены администратором, поэтому перезаписываем их только с
|
||
# явным подтверждением (или --yes для неинтерактивного режима).
|
||
# ---------------------------------------------------------------------------
|
||
if [ "$FRESH_ENV" != "1" ]; then
|
||
APPLY_PRESET_SETTINGS=0
|
||
if [ "$ASSUME_YES" = "1" ]; then
|
||
APPLY_PRESET_SETTINGS=1
|
||
else
|
||
printf "Обновить настройки модулей инстанса (чат, уровень AI) под пресет %s? [Y/n]: " "$PRESET"
|
||
read -r update_settings_answer
|
||
case "$update_settings_answer" in
|
||
n|N|no|No) APPLY_PRESET_SETTINGS=0 ;;
|
||
*) APPLY_PRESET_SETTINGS=1 ;;
|
||
esac
|
||
fi
|
||
if [ "$APPLY_PRESET_SETTINGS" = "1" ]; then
|
||
echo "[install] Применяю настройки модулей инстанса под пресет ${PRESET}"
|
||
docker compose -f "$COMPOSE_FILE" exec -T backend uv run python -m scripts.apply_preset_settings --force
|
||
else
|
||
echo "[install] Настройки модулей инстанса НЕ изменены — сохранены ручные правки администратора"
|
||
fi
|
||
fi
|
||
|
||
VERSION_VALUE="$(cat "$VERSION_FILE")"
|
||
ADMIN_EMAIL="$(get_env_var SEED_ADMIN_EMAIL)"
|
||
ADMIN_PASSWORD="$(get_env_var SEED_ADMIN_PASSWORD)"
|
||
GRAFANA_USER="$(get_env_var GRAFANA_ADMIN_USER)"
|
||
|
||
cat <<EOF
|
||
|
||
============================================================
|
||
VidConf ${VERSION_VALUE} установлен (пресет ${PRESET}).
|
||
============================================================
|
||
Backend: http://localhost/api/health
|
||
Frontend: http://localhost/
|
||
Админ: ${ADMIN_EMAIL:-см. .env SEED_ADMIN_EMAIL} / ${ADMIN_PASSWORD:-см. .env SEED_ADMIN_PASSWORD}
|
||
(смените пароль сразу после первого входа)
|
||
|
||
Мониторинг (Grafana/Prometheus) — отдельный профиль compose, не входит
|
||
в базовые пресеты:
|
||
docker compose -f ${COMPOSE_FILE} --profile monitoring up -d
|
||
Grafana: http://localhost:3001 (${GRAFANA_USER:-admin} / см. .env GRAFANA_ADMIN_PASSWORD)
|
||
|
||
Настройки модулей инстанса (чат, уровень AI) синхронизированы под
|
||
пресет ${PRESET} (админка → Настройки, при желании переключите вручную).
|
||
|
||
Повторный запуск ./install.sh с другим --preset — апгрейд/даунгрейд
|
||
на месте (модели докачиваются, .env обновляется точечно, секреты и
|
||
пользовательские правки сохраняются; настройки модулей обновляются
|
||
только с вашего подтверждения или флагом --yes).
|
||
============================================================
|
||
EOF
|