SaveTest — ДокументацияSaveTest — Документация Сайт
Руководство пользователя
Руководство администратора
Установка и разработка
Руководство пользователя
Руководство администратора
Установка и разработка
  • Установка и разработка

    • Установка и разработка
    • Быстрый старт
    • Установка
    • Обновление
    • Домен и SSL — Nginx (опционально)
    • Разработка плагинов парсеров
    • savetest-cli
Сайт

Установка

Развёртывание выполняется через Docker Compose. Логика сценария: выбрать манифест, создать .env с критичными переменными, запустить стек и выполнить первый вход. Для быстрого ознакомительного запуска без настройки .env см. Быстрый старт.

Требования

  • Установлены Docker Engine и Docker Compose (Docker 20.10+, Compose 2.0+).
  • CPU: от 4 (рекомендуется 8); RAM: от 4 ГБ (рекомендуется 8 ГБ); диск: от ~10 ГБ.
  • Свободны порты 8080 (веб), 8001 (API), 3001 (Allure), 3002 (MCP), 7880 / 7881 / UDP 50000–50100 (LiveKit) — либо измените проброс в манифесте.

Шаг 1. Выбор манифеста

  1. Создайте рабочую директорию и перейдите в неё:
mkdir savetest
cd savetest
  1. Выберите подходящий манифест и сохраните его в этой директории как docker-compose.yml.
ФайлКогда использовать
docker-compose.ymlРекомендуется: продакшен с плагинами, MCP и LiveKit
docker-compose.production.no-plugins.ymlПродакшен без парсеров-плагинов (MCP и LiveKit включены)
docker-compose.production.no-livekit.ymlПродакшен без LiveKit — CallBox только в режиме чата

Продакшен (с плагинами, MCP и LiveKit)

Готовые образы, Allure, webhook-worker, python-parser-plugin, gherkin-parser-plugin, mcp-server и livekit. Backend получает PLUGIN_URLS, MCP_SERVER_URL и LIVEKIT_* по умолчанию.

docker-compose.yml
# Docker Compose конфигурация для продакшн окружения

services:
  postgres:
    image: postgres:15
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-savetest_db}
      POSTGRES_USER: ${POSTGRES_USER:-savetest_user}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-savetest_password}
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8 --lc-collate=en_US.UTF-8 --lc-ctype=en_US.UTF-8"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-savetest_user} -d ${POSTGRES_DB:-savetest_db}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - savetest-network

  python-parser-plugin:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/python-parser-plugin:main
    restart: unless-stopped
    volumes:
      - git_repos:/app/git_repos:ro
    networks:
      - savetest-network
    environment:
      - PLUGIN_NAME=python-parser
      - LOG_LEVEL=${PLUGIN_LOG_LEVEL:-INFO}
    healthcheck:
      test: ["CMD-SHELL", "python -c 'import urllib.request, json; r=urllib.request.urlopen(\"http://localhost:8000/config\", timeout=5); data=json.loads(r.read()); assert data.get(\"language\"), \"no language in config\"'"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s

  gherkin-parser-plugin:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/gherkin-parser-plugin:main
    restart: unless-stopped
    volumes:
      - git_repos:/app/git_repos:ro
    networks:
      - savetest-network
    environment:
      - PLUGIN_NAME=gherkin-parser
      - LOG_LEVEL=${PLUGIN_LOG_LEVEL:-INFO}
    healthcheck:
      test: ["CMD-SHELL", "python -c 'import urllib.request, json; r=urllib.request.urlopen(\"http://localhost:8000/config\", timeout=5); data=json.loads(r.read()); assert data.get(\"language\"), \"no language in config\"'"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s

  allure-service:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/allure-service:main
    container_name: allure-service
    restart: unless-stopped
    ports:
      - "${ALLURE_SERVICE_PORT:-3001}:3001"
    volumes:
      - allure_storage:/allure-storage
    environment:
      - PORT=3001
      - STORAGE_PATH=/allure-storage
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379/0
      - BACKEND_URL=http://backend:8000
    depends_on:
      redis:
        condition: service_healthy
    networks:
      - savetest-network
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  backend:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/backend:main
    restart: unless-stopped
    ports:
      - "${BACKEND_PORT:-8001}:8000"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER:-savetest_user}:${POSTGRES_PASSWORD:-savetest_password}@postgres:5432/${POSTGRES_DB:-savetest_db}
      - REDIS_URL=redis://redis:6379/0
      - SECRET_KEY=${SECRET_KEY:-your-super-secret-key-change-in-production}
      - DEBUG=${DEBUG:-False}
      - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080}
      - UVICORN_WORKERS=${UVICORN_WORKERS:-2}
      - PLUGIN_URLS=${PLUGIN_URLS:-http://python-parser-plugin:8000,http://gherkin-parser-plugin:8000}
      - PLUGIN_TIMEOUT=${PLUGIN_TIMEOUT:-30}
      - LOG_FILE_BACKUP_COUNT=${LOG_FILE_BACKUP_COUNT:-30}
      - LOG_FLUSH_BATCH_SIZE=${LOG_FLUSH_BATCH_SIZE:-10}
      - ALLURE_SERVICE_URL=${ALLURE_SERVICE_URL:-http://allure-service:3001}
      - ALLURE_STORAGE_PATH=/allure-storage
      - MCP_SERVER_URL=http://mcp-server:3000
      - LIVEKIT_URL=http://livekit:7880
      - LIVEKIT_WS_URL=${LIVEKIT_WS_URL:-ws://${STAND_HOST:-localhost}:7880}
      - LIVEKIT_API_KEY=${LIVEKIT_API_KEY:-devkey}
      - LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET:-devsecret_change_me_in_production_please_32chars}
      - CALL_CHAT_UPLOAD_DIR=/app/call_chat_uploads
    volumes:
      - git_repos:/app/git_repos
      - avatars_data:/app/avatars
      - project_avatars_data:/app/project_avatars
      - project_report_assets_data:/app/project_report_assets
      - reports_data:/app/reports
      - result_attachments_data:/app/result_attachments
      - call_chat_uploads_data:/app/call_chat_uploads
      - wiki_sites_data:/app/wiki_sites
      - logs_data:/app/logs
      - app_data:/app/app/__data__
      - allure_storage:/allure-storage
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      python-parser-plugin:
        condition: service_healthy
      gherkin-parser-plugin:
        condition: service_healthy
      allure-service:
        condition: service_healthy
      livekit:
        condition: service_started
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    networks:
      - savetest-network

  mcp-server:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/mcp-server:main
    container_name: mcp-server
    restart: unless-stopped
    environment:
      - PORT=3000
      - SAVETEST_API_URL=http://backend:8000
      - NODE_ENV=production
    ports:
      - "${MCP_SERVER_PORT:-3002}:3000"
    depends_on:
      backend:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    networks:
      - savetest-network

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - savetest-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  livekit:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/livekit:main
    container_name: livekit
    restart: unless-stopped
    environment:
      - "LIVEKIT_KEYS=${LIVEKIT_API_KEY:-devkey}: ${LIVEKIT_API_SECRET:-devsecret_change_me_in_production_please_32chars}"
      - LIVEKIT_WS_URL=${LIVEKIT_WS_URL:-ws://${STAND_HOST:-localhost}:7880}
      - LIVEKIT_RTC_NODE_IP=${LIVEKIT_RTC_NODE_IP:-${STAND_HOST:-}}
      - LIVEKIT_RTC_USE_EXTERNAL_IP=${LIVEKIT_RTC_USE_EXTERNAL_IP:-}
    ports:
      - "${LIVEKIT_PORT:-7880}:7880"
      - "${LIVEKIT_TCP_PORT:-7881}:7881"
      - "${LIVEKIT_UDP_RANGE:-50000-50100}:50000-50100/udp"
    depends_on:
      redis:
        condition: service_healthy
    networks:
      - savetest-network

  webhook-worker:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/webhook-worker:main
    container_name: webhook-worker
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER:-savetest_user}:${POSTGRES_PASSWORD:-savetest_password}@postgres:5432/${POSTGRES_DB:-savetest_db}
      - REDIS_URL=redis://redis:6379/0
      - WORKER_TIMEOUT=${WORKER_TIMEOUT:-30}
      - WORKER_RETRY_COUNT=${WORKER_RETRY_COUNT:-2}
      - WORKER_RETRY_DELAY=${WORKER_RETRY_DELAY:-5}
      - LOG_LEVEL=${WORKER_LOG_LEVEL:-INFO}
      - LOGS_DIR=/app/logs
    volumes:
      - logs_data:/app/logs
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - savetest-network
    healthcheck:
      test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  frontend:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/frontend:main
    restart: unless-stopped
    ports:
      - "${FRONTEND_PORT:-8080}:80"
    depends_on:
      backend:
        condition: service_healthy
      mcp-server:
        condition: service_healthy
    networks:
      - savetest-network
    stop_grace_period: 15s
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

volumes:
  git_repos:
    driver: local
  redis_data:
    driver: local
  avatars_data:
    driver: local
  project_avatars_data:
    driver: local
  project_report_assets_data:
    driver: local
  call_chat_uploads_data:
    driver: local
  reports_data:
    driver: local
  result_attachments_data:
    driver: local
  wiki_sites_data:
    driver: local
  logs_data:
    driver: local
  app_data:
    driver: local
  postgres_data:
  allure_storage:
    driver: local

networks:
  savetest-network:
    driver: bridge

Продакшен без плагинов

Те же сервисы, без контейнеров парсеров; PLUGIN_URLS в backend не задаётся. MCP и LiveKit включены.

docker-compose.production.no-plugins.yml
# Docker Compose конфигурация для продакшн окружения БЕЗ ПЛАГИНОВ

services:
  postgres:
    image: postgres:15
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-savetest_db}
      POSTGRES_USER: ${POSTGRES_USER:-savetest_user}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-savetest_password}
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8 --lc-collate=en_US.UTF-8 --lc-ctype=en_US.UTF-8"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-savetest_user} -d ${POSTGRES_DB:-savetest_db}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - savetest-network

  allure-service:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/allure-service:main
    container_name: allure-service
    restart: unless-stopped
    ports:
      - "${ALLURE_SERVICE_PORT:-3001}:3001"
    volumes:
      - allure_storage:/allure-storage
    environment:
      - PORT=3001
      - STORAGE_PATH=/allure-storage
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379/0
      - BACKEND_URL=http://backend:8000
    depends_on:
      redis:
        condition: service_healthy
    networks:
      - savetest-network
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  backend:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/backend:main
    restart: unless-stopped
    ports:
      - "${BACKEND_PORT:-8001}:8000"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER:-savetest_user}:${POSTGRES_PASSWORD:-savetest_password}@postgres:5432/${POSTGRES_DB:-savetest_db}
      - REDIS_URL=redis://redis:6379/0
      - SECRET_KEY=${SECRET_KEY:-your-super-secret-key-change-in-production}
      - DEBUG=${DEBUG:-False}
      - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080}
      - UVICORN_WORKERS=${UVICORN_WORKERS:-2}
      # PLUGIN_URLS не указан - плагины не будут загружены
      - PLUGIN_TIMEOUT=${PLUGIN_TIMEOUT:-30}
      - LOG_FILE_BACKUP_COUNT=${LOG_FILE_BACKUP_COUNT:-30}
      - LOG_FLUSH_BATCH_SIZE=${LOG_FLUSH_BATCH_SIZE:-10}
      - ALLURE_SERVICE_URL=${ALLURE_SERVICE_URL:-http://allure-service:3001}
      - ALLURE_STORAGE_PATH=/allure-storage
      - MCP_SERVER_URL=http://mcp-server:3000
      - LIVEKIT_URL=http://livekit:7880
      - LIVEKIT_WS_URL=${LIVEKIT_WS_URL:-ws://${STAND_HOST:-localhost}:7880}
      - LIVEKIT_API_KEY=${LIVEKIT_API_KEY:-devkey}
      - LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET:-devsecret_change_me_in_production_please_32chars}
      - CALL_CHAT_UPLOAD_DIR=/app/call_chat_uploads
    volumes:
      - git_repos:/app/git_repos
      - avatars_data:/app/avatars
      - project_avatars_data:/app/project_avatars
      - project_report_assets_data:/app/project_report_assets
      - reports_data:/app/reports
      - result_attachments_data:/app/result_attachments
      - call_chat_uploads_data:/app/call_chat_uploads
      - wiki_sites_data:/app/wiki_sites
      - logs_data:/app/logs
      - app_data:/app/app/__data__
      - allure_storage:/allure-storage
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      allure-service:
        condition: service_healthy
      livekit:
        condition: service_started
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s
    networks:
      - savetest-network

  mcp-server:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/mcp-server:main
    container_name: mcp-server
    restart: unless-stopped
    environment:
      - PORT=3000
      - SAVETEST_API_URL=http://backend:8000
      - NODE_ENV=production
    ports:
      - "${MCP_SERVER_PORT:-3002}:3000"
    depends_on:
      backend:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    networks:
      - savetest-network

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - savetest-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  livekit:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/livekit:main
    container_name: livekit
    restart: unless-stopped
    environment:
      - "LIVEKIT_KEYS=${LIVEKIT_API_KEY:-devkey}: ${LIVEKIT_API_SECRET:-devsecret_change_me_in_production_please_32chars}"
      - LIVEKIT_WS_URL=${LIVEKIT_WS_URL:-ws://${STAND_HOST:-localhost}:7880}
      - LIVEKIT_RTC_NODE_IP=${LIVEKIT_RTC_NODE_IP:-${STAND_HOST:-}}
      - LIVEKIT_RTC_USE_EXTERNAL_IP=${LIVEKIT_RTC_USE_EXTERNAL_IP:-}
    ports:
      - "${LIVEKIT_PORT:-7880}:7880"
      - "${LIVEKIT_TCP_PORT:-7881}:7881"
      - "${LIVEKIT_UDP_RANGE:-50000-50100}:50000-50100/udp"
    depends_on:
      redis:
        condition: service_healthy
    networks:
      - savetest-network

  webhook-worker:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/webhook-worker:main
    container_name: webhook-worker
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER:-savetest_user}:${POSTGRES_PASSWORD:-savetest_password}@postgres:5432/${POSTGRES_DB:-savetest_db}
      - REDIS_URL=redis://redis:6379/0
      - WORKER_TIMEOUT=${WORKER_TIMEOUT:-30}
      - WORKER_RETRY_COUNT=${WORKER_RETRY_COUNT:-2}
      - WORKER_RETRY_DELAY=${WORKER_RETRY_DELAY:-5}
      - LOG_LEVEL=${WORKER_LOG_LEVEL:-INFO}
      - LOGS_DIR=/app/logs
    volumes:
      - logs_data:/app/logs
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - savetest-network
    healthcheck:
      test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  frontend:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/frontend:main
    restart: unless-stopped
    ports:
      - "${FRONTEND_PORT:-8080}:80"
    depends_on:
      backend:
        condition: service_healthy
      mcp-server:
        condition: service_healthy
    networks:
      - savetest-network
    stop_grace_period: 15s
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

volumes:
  git_repos:
    driver: local
  redis_data:
    driver: local
  avatars_data:
    driver: local
  project_avatars_data:
    driver: local
  project_report_assets_data:
    driver: local
  call_chat_uploads_data:
    driver: local
  reports_data:
    driver: local
  result_attachments_data:
    driver: local
  wiki_sites_data:
    driver: local
  logs_data:
    driver: local
  app_data:
    driver: local
  postgres_data:
  allure_storage:
    driver: local

networks:
  savetest-network:
    driver: bridge

Продакшен без LiveKit

Полный стек с MCP, но без контейнера LiveKit — голос в CallBox отключён, текстовый чат работает.

docker-compose.production.no-livekit.yml
# Docker Compose для продакшн без LiveKit (CallBox — только текстовый чат).
# MCP-сервер включён. Голос отключён: пустые LIVEKIT_* → LIVEKIT_ENABLED=false.

services:
  postgres:
    image: postgres:15
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-savetest_db}
      POSTGRES_USER: ${POSTGRES_USER:-savetest_user}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-savetest_password}
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=en_US.UTF-8 --lc-collate=en_US.UTF-8 --lc-ctype=en_US.UTF-8"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-savetest_user} -d ${POSTGRES_DB:-savetest_db}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - savetest-network

  python-parser-plugin:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/python-parser-plugin:main
    restart: unless-stopped
    volumes:
      - git_repos:/app/git_repos:ro
    networks:
      - savetest-network
    environment:
      - PLUGIN_NAME=python-parser
      - LOG_LEVEL=${PLUGIN_LOG_LEVEL:-INFO}
    healthcheck:
      test: ["CMD-SHELL", "python -c 'import urllib.request, json; r=urllib.request.urlopen(\"http://localhost:8000/config\", timeout=5); data=json.loads(r.read()); assert data.get(\"language\"), \"no language in config\"'"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s

  gherkin-parser-plugin:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/gherkin-parser-plugin:main
    restart: unless-stopped
    volumes:
      - git_repos:/app/git_repos:ro
    networks:
      - savetest-network
    environment:
      - PLUGIN_NAME=gherkin-parser
      - LOG_LEVEL=${PLUGIN_LOG_LEVEL:-INFO}
    healthcheck:
      test: ["CMD-SHELL", "python -c 'import urllib.request, json; r=urllib.request.urlopen(\"http://localhost:8000/config\", timeout=5); data=json.loads(r.read()); assert data.get(\"language\"), \"no language in config\"'"]
      interval: 10s
      timeout: 10s
      retries: 5
      start_period: 30s

  allure-service:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/allure-service:main
    container_name: allure-service
    restart: unless-stopped
    ports:
      - "${ALLURE_SERVICE_PORT:-3001}:3001"
    volumes:
      - allure_storage:/allure-storage
    environment:
      - PORT=3001
      - STORAGE_PATH=/allure-storage
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379/0
      - BACKEND_URL=http://backend:8000
    depends_on:
      redis:
        condition: service_healthy
    networks:
      - savetest-network
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  backend:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/backend:main
    restart: unless-stopped
    ports:
      - "${BACKEND_PORT:-8001}:8000"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER:-savetest_user}:${POSTGRES_PASSWORD:-savetest_password}@postgres:5432/${POSTGRES_DB:-savetest_db}
      - REDIS_URL=redis://redis:6379/0
      - SECRET_KEY=${SECRET_KEY:-your-super-secret-key-change-in-production}
      - DEBUG=${DEBUG:-False}
      - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost:3000,http://127.0.0.1:3000,http://localhost:8080,http://127.0.0.1:8080}
      - UVICORN_WORKERS=${UVICORN_WORKERS:-2}
      - PLUGIN_URLS=${PLUGIN_URLS:-http://python-parser-plugin:8000,http://gherkin-parser-plugin:8000}
      - PLUGIN_TIMEOUT=${PLUGIN_TIMEOUT:-30}
      - LOG_FILE_BACKUP_COUNT=${LOG_FILE_BACKUP_COUNT:-30}
      - LOG_FLUSH_BATCH_SIZE=${LOG_FLUSH_BATCH_SIZE:-10}
      - ALLURE_SERVICE_URL=${ALLURE_SERVICE_URL:-http://allure-service:3001}
      - ALLURE_STORAGE_PATH=/allure-storage
      - MCP_SERVER_URL=http://mcp-server:3000
      - LIVEKIT_URL=
      - LIVEKIT_WS_URL=
      - LIVEKIT_API_KEY=
      - LIVEKIT_API_SECRET=
      - CALL_CHAT_UPLOAD_DIR=/app/call_chat_uploads
    volumes:
      - git_repos:/app/git_repos
      - avatars_data:/app/avatars
      - project_avatars_data:/app/project_avatars
      - project_report_assets_data:/app/project_report_assets
      - reports_data:/app/reports
      - result_attachments_data:/app/result_attachments
      - call_chat_uploads_data:/app/call_chat_uploads
      - wiki_sites_data:/app/wiki_sites
      - logs_data:/app/logs
      - app_data:/app/app/__data__
      - allure_storage:/allure-storage
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      python-parser-plugin:
        condition: service_healthy
      gherkin-parser-plugin:
        condition: service_healthy
      allure-service:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    networks:
      - savetest-network

  mcp-server:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/mcp-server:main
    container_name: mcp-server
    restart: unless-stopped
    environment:
      - PORT=3000
      - SAVETEST_API_URL=http://backend:8000
      - NODE_ENV=production
    ports:
      - "${MCP_SERVER_PORT:-3002}:3000"
    depends_on:
      backend:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    networks:
      - savetest-network

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    networks:
      - savetest-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  webhook-worker:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/webhook-worker:main
    container_name: webhook-worker
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER:-savetest_user}:${POSTGRES_PASSWORD:-savetest_password}@postgres:5432/${POSTGRES_DB:-savetest_db}
      - REDIS_URL=redis://redis:6379/0
      - WORKER_TIMEOUT=${WORKER_TIMEOUT:-30}
      - WORKER_RETRY_COUNT=${WORKER_RETRY_COUNT:-2}
      - WORKER_RETRY_DELAY=${WORKER_RETRY_DELAY:-5}
      - LOG_LEVEL=${WORKER_LOG_LEVEL:-INFO}
      - LOGS_DIR=/app/logs
    volumes:
      - logs_data:/app/logs
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - savetest-network
    healthcheck:
      test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

  frontend:
    image: cr.yandex/crp8hln0vgikkl8djavt/save-test/frontend:main
    restart: unless-stopped
    ports:
      - "${FRONTEND_PORT:-8080}:80"
    depends_on:
      backend:
        condition: service_healthy
      mcp-server:
        condition: service_healthy
    networks:
      - savetest-network
    stop_grace_period: 15s
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

volumes:
  git_repos:
    driver: local
  redis_data:
    driver: local
  avatars_data:
    driver: local
  project_avatars_data:
    driver: local
  project_report_assets_data:
    driver: local
  call_chat_uploads_data:
    driver: local
  reports_data:
    driver: local
  result_attachments_data:
    driver: local
  wiki_sites_data:
    driver: local
  logs_data:
    driver: local
  app_data:
    driver: local
  postgres_data:
  allure_storage:
    driver: local

networks:
  savetest-network:
    driver: bridge

MCP и LiveKit в рекомендуемом стеке

В основном манифесте уже подняты:

  • mcp-server — AI-ассистенты (Cursor, VS Code). Endpoint: https://<домен>/mcp (проксируется frontend-nginx). Пользователю нужен API-токен (sat_…) из профиля. Пример для Cursor:
{
  "mcpServers": {
    "savetest": {
      "url": "https://your-domain.com/mcp",
      "headers": {
        "Authorization": "Bearer ${env:SAVETEST_API_TOKEN}"
      }
    }
  }
}
  • livekit — голосовая связь в CallBox (WebRTC). Для стенда в LAN или за NAT добавьте в .env:
STAND_HOST=192.168.1.50
LIVEKIT_API_KEY=your-livekit-key
LIVEKIT_API_SECRET=your-livekit-secret-at-least-32-chars

Для доступа из интернета пробросьте TCP 7880, 7881 и UDP 50000–50100, задайте LIVEKIT_WS_URL и LIVEKIT_RTC_NODE_IP (публичный IP). Для микрофона в браузере нужен HTTPS у фронтенда.

Подробнее о прокси и SSL — в Домен и SSL — Nginx.

Шаг 2. Создание .env (критичные переменные)

Создайте файл .env в каталоге с docker-compose.yml и задайте минимум критичных параметров:

POSTGRES_PASSWORD=your-secure-password
SECRET_KEY=your-super-secret-key

Минимум требований для продакшена:

  • POSTGRES_PASSWORD — надёжный пароль БД.
  • SECRET_KEY — уникальный секрет приложения (рекомендуется не короче ~32 символов).

При необходимости расширьте .env дополнительными параметрами из раздела Параметры окружения.

Шаг 3. Запуск и доступ

Запустите контейнеры:

docker compose up -d
docker compose ps

Интерфейс по умолчанию: http://localhost:8080 (порт задаётся пробросом у frontend, см. манифест).

Шаг 4. Первый вход

При первом входе создайте суперадминистратора:

Создание суперадминистратора при первом входе (светлая тема)Создание суперадминистратора при первом входе (тёмная тема)

Важно: Для внешнего доступа по домену и HTTPS используйте отдельный материал: Домен и SSL — Nginx (опционально).

Параметры окружения

PostgreSQL

ПеременнаяОписаниеТипПо умолчаниюОбязательноПримечания
POSTGRES_DBИмя базы данныхstringsavetest_dbНет
POSTGRES_USERПользователь БДstringsavetest_userНет
POSTGRES_PASSWORDПароль БДstringsavetest_passwordНетВ продакшене — надёжный пароль
POSTGRES_INITDB_ARGSАргументы инициализации БДstring"--encoding=UTF8 --locale=en_US.UTF-8 --lc-collate=en_US.UTF-8 --lc-ctype=en_US.UTF-8"НетОбычно не меняют

Backend

ПеременнаяОписаниеТипПо умолчаниюОбязательноПримечания
DATABASE_URLURL PostgreSQLstringсм. манифестНетpostgresql://user:password@host:port/database
REDIS_URLURL Redisstringredis://redis:6379/0Нет
SECRET_KEYСекрет JWT и шифрованияstringсм. манифестДа в продеМинимум ~32 символа; python -c "import secrets; print(secrets.token_urlsafe(32))"
DEBUGРежим отладкиboolFalseНетВ проде — False
ALLOWED_ORIGINSCORSstringlocalhost* в манифестеНетURL через запятую или JSON-массив
UVICORN_WORKERSЧисло workersint2НетЗависит от нагрузки
PLUGIN_URLSURL плагинов через запятуюstringсм. productionНетИли PLUGIN_1_URL, PLUGIN_2_URL, …; метаданные — GET /config
PLUGIN_TIMEOUTТаймаут к плагинам (сек)int30Нет
LOG_FILE_BACKUP_COUNTХранение логов (дней)int30Нет
LOG_FLUSH_BATCH_SIZEЗаписей до flushint10Нет
ALLURE_SERVICE_URLURL Allurestringhttp://allure-service:3001Нет
ALLURE_STORAGE_PATHПуть в контейнере backendstring/allure-storageНетСогласовать с volume
APP_HOSTBind APIstring0.0.0.0Нет
APP_PORTПорт API в контейнереint8000Нет
ACCESS_TOKEN_EXPIRE_MINUTESЖизнь access-токена (мин)int10080Нет
REFRESH_TOKEN_EXPIRE_DAYSЖизнь refresh-токена (дни)int14Нет
MCP_SERVER_URLВнутренний URL MCPstringhttp://mcp-server:3000НетВ рекомендуемом манифесте
CALL_CHAT_UPLOAD_DIRВложения чата CallBoxstring/app/call_chat_uploadsНетVolume call_chat_uploads_data
LIVEKIT_URLHTTP API LiveKitstringhttp://livekit:7880НетПусто в no-livekit манифесте
LIVEKIT_WS_URLWS URL для клиентовstringпустоНетwss://… в проде
LIVEKIT_API_KEYКлюч LiveKitstringпустоНетВместе с secret включает голос
LIVEKIT_API_SECRETСекрет LiveKitstringпустоНет≥32 символов в проде
STAND_HOSTIP/хост стендаstring—НетУпрощает LAN-настройку LiveKit
LIVEKIT_RTC_NODE_IPПубличный IP для ICEstring—НетWAN при NAT; только IP

Проброс портов (Compose)

ПеременнаяОписаниеПо умолчанию
FRONTEND_PORTПорт веб-интерфейса на хосте8080
BACKEND_PORTПорт API на хосте8001
ALLURE_SERVICE_PORTПорт Allure на хосте3001
MCP_SERVER_PORTПорт MCP на хосте3002
LIVEKIT_PORTСигналинг LiveKit (TCP)7880
LIVEKIT_TCP_PORTICE/TCP LiveKit7881
LIVEKIT_UDP_RANGEUDP-медиа LiveKit50000-50100

Allure Service

ПеременнаяОписаниеПо умолчанию
PORTПорт в контейнере3001
STORAGE_PATHКаталог отчётов/allure-storage
NODE_ENVОкружение Node.jsproduction

Webhook Worker

ПеременнаяОписаниеПо умолчаниюОбязательно
DATABASE_URLPostgreSQL—Да
REDIS_URLRedisredis://redis:6379/0Нет
WORKER_TIMEOUTТаймаут задачи (сек)30Нет
WORKER_RETRY_COUNTПовторы2Нет
WORKER_RETRY_DELAYПауза (сек)5Нет
LOG_LEVEL / WORKER_LOG_LEVELУровень логовINFOНет
LOGS_DIRКаталог логов/app/logsНет

Контейнеры плагинов

ПеременнаяОписаниеПо умолчанию
PLUGIN_NAMEИмя в логахиз образа
PLUGIN_LOG_LEVELУровень логовINFO

MCP Server (опционально)

ПеременнаяОписаниеПо умолчанию
SAVETEST_API_URLURL бэкендаhttp://backend:8000
PORTПорт в контейнере3000

LiveKit (опционально)

ПеременнаяОписаниеПо умолчанию
LIVEKIT_KEYSключ: секретиз LIVEKIT_API_KEY / SECRET
LIVEKIT_RTC_USE_EXTERNAL_IPВнешний IP в ICEавто

Redis

Для образа redis:7-alpine дополнительные переменные в типовой схеме не задаются.

Назад
Быстрый старт
Далее
Обновление