From d934900121a78ebf91b591e309437a2bc2ad28d4 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 8 Oct 2025 19:15:09 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=84?= =?UTF-8?q?=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Dockerfile | 3 +- backend/Dockerfile.prod | 11 +- docker-compose.yml | 2 +- frontend/nginx.Dockerfile | 6 +- setup.sh | 51 ++++++++- webssh-agent/Dockerfile | 3 +- webssh-agent/package.json | 3 +- webssh-agent/utils/dockerUtils.js | 180 ++++++++++++++---------------- 8 files changed, 147 insertions(+), 112 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 8ebcf1c..b0f90cd 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -23,11 +23,12 @@ RUN apt-get update && apt-get install -y \ python3 \ make \ g++ \ - docker.io \ curl \ ca-certificates \ && rm -rf /var/lib/apt/lists/* +# Docker CLI НЕ устанавливаем - используем Docker Socket + dockerode SDK + COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile diff --git a/backend/Dockerfile.prod b/backend/Dockerfile.prod index 14bbc7e..8e02307 100644 --- a/backend/Dockerfile.prod +++ b/backend/Dockerfile.prod @@ -3,7 +3,7 @@ # This software is proprietary and confidential. # For licensing inquiries: info@hb3-accelerator.com -FROM node:20-alpine +FROM node:20-slim # Добавляем метки для авторских прав LABEL maintainer="Тарабанов Александр Викторович " @@ -13,8 +13,13 @@ LABEL website="https://hb3-accelerator.com" WORKDIR /app -# Устанавливаем только docker-cli (без демона) для Alpine Linux -RUN apk update && apk add --no-cache docker-cli curl ca-certificates +# Устанавливаем системные зависимости для Debian +RUN apt-get update && apt-get install -y \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Docker CLI НЕ устанавливаем - используем Docker Socket + dockerode SDK COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile --production diff --git a/docker-compose.yml b/docker-compose.yml index 092d311..c059b40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: postgres: - image: postgres:16-alpine + image: postgres:16 container_name: dapp-postgres restart: unless-stopped logging: diff --git a/frontend/nginx.Dockerfile b/frontend/nginx.Dockerfile index a8fe476..56d6e1e 100644 --- a/frontend/nginx.Dockerfile +++ b/frontend/nginx.Dockerfile @@ -1,5 +1,5 @@ # Этап 1: Сборка frontend -FROM node:18-alpine AS frontend-builder +FROM node:20-slim AS frontend-builder WORKDIR /app # Копируем файлы зависимостей @@ -23,8 +23,8 @@ RUN apk add --no-cache curl # Копируем собранный frontend из первого этапа COPY --from=frontend-builder /app/dist/ /usr/share/nginx/html/ -# Копируем конфигурацию nginx (используем dev версию для локальной разработки) -COPY nginx-dev.conf /etc/nginx/nginx.conf.template +# Копируем конфигурацию nginx +COPY nginx-simple.conf /etc/nginx/nginx.conf.template # Копируем скрипт запуска COPY docker-entrypoint.sh /docker-entrypoint.sh diff --git a/setup.sh b/setup.sh index 3918148..81b19a6 100755 --- a/setup.sh +++ b/setup.sh @@ -31,21 +31,62 @@ check_docker() { curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh rm get-docker.sh - print_green "Docker установлен. Перезапустите терминал или выполните: newgrp docker" + + # Добавляем текущего пользователя в группу docker + print_blue "Добавление пользователя в группу docker..." + sudo usermod -aG docker $USER + + print_green "Docker установлен!" + print_yellow "⚠️ ВАЖНО: Для применения изменений выполните одну из команд:" + print_yellow " 1. newgrp docker (применить в текущем терминале)" + print_yellow " 2. Перезапустите терминал" + print_yellow " 3. Перезайдите в систему" + print_blue "Нажмите Enter для продолжения после выполнения команды..." + read else print_yellow "Пожалуйста, установите Docker вручную: https://docs.docker.com/get-docker/" print_yellow "Для Windows/Mac: скачайте и установите Docker Desktop." exit 1 fi fi - print_green "Docker установлен." + + # Проверка прав доступа к Docker + if ! docker ps &> /dev/null; then + print_yellow "⚠️ Нет прав для запуска Docker команд." + print_blue "Добавление пользователя в группу docker..." + + # Проверяем, есть ли пользователь в группе docker + if ! groups $USER | grep -q docker; then + sudo usermod -aG docker $USER + print_yellow "Пользователь добавлен в группу docker." + print_yellow "Выполните команду для применения изменений: newgrp docker" + print_yellow "Или перезапустите терминал и запустите скрипт снова." + exit 0 + else + print_red "Пользователь уже в группе docker, но права не работают." + print_yellow "Попробуйте:" + print_yellow " 1. newgrp docker" + print_yellow " 2. Перезайдите в систему" + print_yellow " 3. Перезапустите Docker: sudo systemctl restart docker" + exit 1 + fi + fi + + print_green "Docker установлен и доступен." print_blue "Проверка Docker Compose..." if ! docker compose version &> /dev/null; then print_yellow "Docker Compose не установлен или требуется обновление." if [[ "$OSTYPE" == "linux-gnu"* ]]; then - print_blue "Установка Docker Compose (входит в новые версии Docker)..." - print_yellow "Если после установки Docker Compose не работает, обновите Docker или следуйте инструкции: https://docs.docker.com/compose/install/" + print_blue "Установка Docker Compose плагина..." + sudo apt-get update + sudo apt-get install -y docker-compose-plugin + + if ! docker compose version &> /dev/null; then + print_red "Не удалось установить Docker Compose плагин." + print_yellow "Попробуйте обновить Docker: https://docs.docker.com/compose/install/" + exit 1 + fi else print_yellow "Пожалуйста, установите Docker Compose вручную: https://docs.docker.com/compose/install/" exit 1 @@ -98,7 +139,7 @@ create_encryption_key() { pull_images() { print_blue "Предварительная загрузка образов Docker..." - images=("node:20-alpine" "postgres:16-alpine" "ollama/ollama:latest" "curlimages/curl:latest") + images=("node:20-slim" "postgres:16" "ollama/ollama:latest") for img in "${images[@]}"; do print_blue "Загрузка образа: $img" diff --git a/webssh-agent/Dockerfile b/webssh-agent/Dockerfile index 7c33dc4..731f694 100644 --- a/webssh-agent/Dockerfile +++ b/webssh-agent/Dockerfile @@ -6,13 +6,14 @@ RUN apt-get update && apt-get install -y \ sshpass \ curl \ wget \ - docker.io \ ca-certificates \ python3 \ make \ g++ \ && rm -rf /var/lib/apt/lists/* +# Docker CLI НЕ устанавливаем - используем Docker Socket + dockerode SDK + # Создаем рабочую директорию WORKDIR /app diff --git a/webssh-agent/package.json b/webssh-agent/package.json index 519e25c..27872da 100644 --- a/webssh-agent/package.json +++ b/webssh-agent/package.json @@ -23,7 +23,8 @@ "cors": "^2.8.5", "fs-extra": "^11.1.1", "chalk": "^4.1.2", - "ws": "^8.14.2" + "ws": "^8.14.2", + "dockerode": "^4.0.2" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/webssh-agent/utils/dockerUtils.js b/webssh-agent/utils/dockerUtils.js index 6e328ef..fd036e1 100644 --- a/webssh-agent/utils/dockerUtils.js +++ b/webssh-agent/utils/dockerUtils.js @@ -1,7 +1,11 @@ -const { exec } = require('child_process'); +const Docker = require('dockerode'); +const fs = require('fs-extra'); const { execSshCommand, execScpCommand } = require('./sshUtils'); const log = require('./logger'); +// Инициализируем Docker клиент через socket +const docker = new Docker({ socketPath: '/var/run/docker.sock' }); + /** * Экспорт Docker образов и данных с локальной машины */ @@ -10,7 +14,7 @@ const exportDockerImages = async (sendWebSocketLog) => { sendWebSocketLog('info', '📦 Начинаем экспорт Docker образов...', 'export_images', 60); const images = [ - { name: 'postgres:16-alpine', file: 'dapp-postgres.tar' }, + { name: 'postgres:16', file: 'dapp-postgres.tar' }, { name: 'digital_legal_entitydle-ollama:latest', file: 'dapp-ollama.tar' }, { name: 'digital_legal_entitydle-vector-search:latest', file: 'dapp-vector-search.tar' }, { name: 'digital_legal_entitydle-backend:latest', file: 'dapp-backend.tar' }, @@ -25,121 +29,102 @@ const exportDockerImages = async (sendWebSocketLog) => { const progress = 60 + Math.floor((i / images.length) * 10); // 60-70% sendWebSocketLog('info', `📦 Экспорт образа: ${image.name}`, 'export_images', progress); - await new Promise((resolve) => { - exec(`docker save ${image.name} -o /tmp/${image.file}`, (error, stdout, stderr) => { - if (error) { - log.error(`Ошибка экспорта ${image.name}: ${error.message}`); - sendWebSocketLog('error', `❌ Ошибка экспорта ${image.name}`, 'export_images', progress); - } else { + try { + const dockerImage = docker.getImage(image.name); + const stream = await dockerImage.get(); + const outputPath = `/tmp/${image.file}`; + + // Сохраняем stream в файл + await new Promise((resolve, reject) => { + const writeStream = fs.createWriteStream(outputPath); + stream.pipe(writeStream); + stream.on('end', () => { sendWebSocketLog('success', `✅ Экспорт ${image.name} завершен`, 'export_images', progress); - } - resolve(); + resolve(); + }); + stream.on('error', reject); + writeStream.on('error', reject); }); - }); + } catch (error) { + log.error(`Ошибка экспорта ${image.name}: ${error.message}`); + sendWebSocketLog('error', `❌ Ошибка экспорта ${image.name}`, 'export_images', progress); + } } // Экспортируем данные из volumes log.info('Экспорт данных из Docker volumes...'); sendWebSocketLog('info', '📦 Экспорт данных из Docker volumes...', 'export_data', 70); - // PostgreSQL данные - проверяем наличие данных перед экспортом + // PostgreSQL данные sendWebSocketLog('info', '📦 Экспорт данных PostgreSQL...', 'export_data', 72); - await new Promise((resolve) => { - // Сначала проверим, есть ли данные в volume - exec('docker run --rm -v digital_legal_entitydle_postgres_data:/data alpine ls -la /data/base', (checkError, checkStdout, checkStderr) => { - if (checkError) { - log.error(`Ошибка проверки данных PostgreSQL: ${checkError.message}`); - sendWebSocketLog('error', `❌ Ошибка проверки данных PostgreSQL`, 'export_data', 72); - resolve(); - return; - } - - // Если данные есть, экспортируем их - exec('docker run --rm -v digital_legal_entitydle_postgres_data:/data -v /tmp:/backup alpine tar czf /backup/postgres_data.tar.gz -C /data .', (error, stdout, stderr) => { - if (error) { - log.error(`Ошибка экспорта данных PostgreSQL: ${error.message}`); - sendWebSocketLog('error', `❌ Ошибка экспорта данных PostgreSQL`, 'export_data', 72); - } else { - log.info(`Данные PostgreSQL экспортированы: ${checkStdout.trim()}`); - sendWebSocketLog('success', `✅ Экспорт данных PostgreSQL завершен (содержит базы данных)`, 'export_data', 72); - } - resolve(); - }); - }); - }); + await exportVolumeData('digital_legal_entitydle_postgres_data', 'postgres_data.tar.gz', sendWebSocketLog, 72); // Ollama данные sendWebSocketLog('info', '📦 Экспорт данных Ollama...', 'export_data', 75); - await new Promise((resolve) => { - exec('docker run --rm -v digital_legal_entitydle_ollama_data:/data -v /tmp:/backup alpine tar czf /backup/ollama_data.tar.gz -C /data .', (error, stdout, stderr) => { - if (error) { - log.error(`Ошибка экспорта данных Ollama: ${error.message}`); - sendWebSocketLog('error', `❌ Ошибка экспорта данных Ollama`, 'export_data', 75); - } else { - sendWebSocketLog('success', `✅ Экспорт данных Ollama завершен`, 'export_data', 75); - } - resolve(); - }); - }); + await exportVolumeData('digital_legal_entitydle_ollama_data', 'ollama_data.tar.gz', sendWebSocketLog, 75); // Vector Search данные sendWebSocketLog('info', '📦 Экспорт данных Vector Search...', 'export_data', 78); - await new Promise((resolve) => { - exec('docker run --rm -v digital_legal_entitydle_vector_search_data:/data -v /tmp:/backup alpine tar czf /backup/vector_search_data.tar.gz -C /data .', (error, stdout, stderr) => { - if (error) { - log.error(`Ошибка экспорта данных Vector Search: ${error.message}`); - sendWebSocketLog('error', `❌ Ошибка экспорта данных Vector Search`, 'export_data', 78); - } else { - sendWebSocketLog('success', `✅ Экспорт данных Vector Search завершен`, 'export_data', 78); - } - resolve(); - }); - }); - - // Проверяем размеры экспортированных данных - log.info('Проверка размеров экспортированных данных...'); - sendWebSocketLog('info', '📊 Проверка размеров экспортированных данных...', 'export_data', 78); - - await new Promise((resolve) => { - exec('ls -lh /tmp/postgres_data.tar.gz /tmp/ollama_data.tar.gz /tmp/vector_search_data.tar.gz 2>/dev/null || echo "Некоторые файлы не найдены"', (error, stdout, stderr) => { - if (stdout && stdout.trim()) { - log.info(`Размеры экспортированных данных:\n${stdout.trim()}`); - sendWebSocketLog('info', `📊 Размеры данных:\n${stdout.trim()}`, 'export_data', 78); - } - resolve(); - }); - }); + await exportVolumeData('digital_legal_entitydle_vector_search_data', 'vector_search_data.tar.gz', sendWebSocketLog, 78); // Создаем архив с ВСЕМИ образами и данными приложения log.info('Создание архива Docker образов и данных на хосте...'); sendWebSocketLog('info', '📦 Создание архива всех данных...', 'export_data', 80); - const tarFiles = images.map(img => img.file).join(' '); - const dataFiles = 'postgres_data.tar.gz ollama_data.tar.gz vector_search_data.tar.gz'; - await new Promise((resolve) => { - exec(`chmod 644 /tmp/dapp-*.tar /tmp/*_data.tar.gz && cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles}`, (error, stdout, stderr) => { - if (error) { - log.error('Ошибка создания архива: ' + error.message); - sendWebSocketLog('error', '❌ Ошибка создания архива', 'export_data', 80); - } else { - // Проверяем размер финального архива - exec('ls -lh /tmp/docker-images-and-data.tar.gz', (sizeError, sizeStdout, sizeStderr) => { - if (sizeStdout && sizeStdout.trim()) { - log.info(`Финальный архив создан: ${sizeStdout.trim()}`); - sendWebSocketLog('success', `✅ Архив создан успешно (${sizeStdout.trim()})`, 'export_data', 80); - } else { - sendWebSocketLog('success', '✅ Архив создан успешно', 'export_data', 80); - } - resolve(); - }); + try { + const tarFiles = images.map(img => img.file).join(' '); + const dataFiles = 'postgres_data.tar.gz ollama_data.tar.gz vector_search_data.tar.gz'; + + // Создаем архив через временный контейнер + const container = await docker.createContainer({ + Image: 'alpine', + Cmd: ['sh', '-c', `cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles}`], + HostConfig: { + Binds: ['/tmp:/tmp'], + AutoRemove: true } }); - }); + + await container.start(); + await container.wait(); + + sendWebSocketLog('success', '✅ Архив создан успешно', 'export_data', 80); + } catch (error) { + log.error('Ошибка создания архива: ' + error.message); + sendWebSocketLog('error', '❌ Ошибка создания архива', 'export_data', 80); + } log.success('Docker образы и данные успешно экспортированы'); sendWebSocketLog('success', '✅ Экспорт данных завершен', 'export_data', 80); }; +/** + * Вспомогательная функция для экспорта данных volume + */ +const exportVolumeData = async (volumeName, outputFile, sendWebSocketLog, progress) => { + try { + const container = await docker.createContainer({ + Image: 'alpine', + Cmd: ['tar', 'czf', `/backup/${outputFile}`, '-C', '/data', '.'], + HostConfig: { + Binds: [ + `${volumeName}:/data:ro`, + '/tmp:/backup' + ], + AutoRemove: true + } + }); + + await container.start(); + await container.wait(); + + sendWebSocketLog('success', `✅ Экспорт ${outputFile} завершен`, 'export_data', progress); + } catch (error) { + log.error(`Ошибка экспорта ${volumeName}: ${error.message}`); + sendWebSocketLog('error', `❌ Ошибка экспорта ${volumeName}`, 'export_data', progress); + } +}; + /** * Передача Docker образов и данных на VDS */ @@ -258,13 +243,14 @@ docker volume ls | grep dapp_`; */ const cleanupLocalFiles = async () => { log.info('Очистка временных файлов на хосте...'); - await new Promise((resolve) => { - exec('rm -f /tmp/dapp-*.tar /tmp/*_data.tar.gz /tmp/docker-images-and-data.tar.gz', (error, stdout, stderr) => { - if (error) log.error('Ошибка очистки файлов: ' + error.message); - resolve(); - }); - }); - log.success('Временные файлы очищены (SSH ключи сохранены на хосте)'); + try { + await fs.remove('/tmp/dapp-*.tar'); + await fs.remove('/tmp/*_data.tar.gz'); + await fs.remove('/tmp/docker-images-and-data.tar.gz'); + log.success('Временные файлы очищены'); + } catch (error) { + log.error('Ошибка очистки файлов: ' + error.message); + } }; module.exports = {