Автоматическая публикация приложения в интернете через SSH-туннель.
Автоматическая публикация приложения в интернете.
✓ Быстрое подключение
✓ Безопасно
diff --git a/frontend/src/views/settings/Interface/InterfaceWebSshView.vue b/frontend/src/views/settings/Interface/InterfaceWebSshView.vue
index 0dfca44..1a0cf8c 100644
--- a/frontend/src/views/settings/Interface/InterfaceWebSshView.vue
+++ b/frontend/src/views/settings/Interface/InterfaceWebSshView.vue
@@ -21,8 +21,7 @@
/>
-
WEB SSH: интеграция и настройки
-
Автоматическая публикация приложения через SSH-туннель и NGINX.
+
Настройка VDS Сервер
diff --git a/sync-to-vds.sh b/sync-to-vds.sh
index 18cef90..fa34439 100755
--- a/sync-to-vds.sh
+++ b/sync-to-vds.sh
@@ -10,7 +10,7 @@ NC='\033[0m' # No Color
echo -e "${GREEN}🔄 Синхронизация кода с VDS...${NC}"
# Параметры VDS (из настроек)
-VDS_HOST="185.221.214.140"
+VDS_HOST="185.26.121.127"
VDS_USER="root"
VDS_PORT="22"
VDS_PATH="/home/docker/dapp"
diff --git a/vector-search/Dockerfile b/vector-search/Dockerfile
index c2af119..d62c3cf 100644
--- a/vector-search/Dockerfile
+++ b/vector-search/Dockerfile
@@ -18,7 +18,8 @@ LABEL website="https://hb3-accelerator.com"
WORKDIR /app
COPY requirements.txt ./
-RUN pip install --no-cache-dir -r requirements.txt
+# Увеличиваем таймауты pip для медленного интернета
+RUN pip install --no-cache-dir --default-timeout=300 --retries=5 -r requirements.txt
COPY . .
EXPOSE 8001
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8001"]
\ No newline at end of file
diff --git a/webssh-agent/agent.js b/webssh-agent/agent.js
index 0781d99..eb1d5e0 100644
--- a/webssh-agent/agent.js
+++ b/webssh-agent/agent.js
@@ -494,7 +494,8 @@ findtime = 3600
log.success('Директория для ключа шифрования подготовлена');
// 9.1. Передача ключа шифрования на VDS
- sendWebSocketLog('info', '🔐 Передача ключа шифрования на VDS...', 'encryption_key', 36);
+ // Прогресс после установки Docker (55%), двигаемся вперёд, а не назад
+ sendWebSocketLog('info', '🔐 Передача ключа шифрования на VDS...', 'encryption_key', 56);
log.info('🔐 Передача ключа шифрования на VDS...');
try {
@@ -592,14 +593,16 @@ findtime = 3600
if (verifyResult.code === 0) {
log.success('✅ Ключ шифрования успешно передан на VDS');
log.info(`📋 Информация о ключе на VDS: ${verifyResult.stdout.trim()}`);
- sendWebSocketLog('success', '✅ Ключ шифрования передан на VDS', 'encryption_key', 37);
+ // Делаем прогресс строго больше предыдущего шага Docker (55%)
+ sendWebSocketLog('success', '✅ Ключ шифрования передан на VDS', 'encryption_key', 57);
} else {
throw new Error(`Не удалось проверить передачу ключа шифрования: ${verifyResult.stderr || verifyResult.stdout}`);
}
} catch (error) {
log.error('❌ Ошибка передачи ключа шифрования: ' + error.message);
log.error('📋 Детали ошибки:', error.stack);
- sendWebSocketLog('error', `❌ Ошибка передачи ключа шифрования: ${error.message}`, 'encryption_key', 37);
+ // Даже при ошибке не откатываем прогресс назад относительно предыдущих шагов
+ sendWebSocketLog('error', `❌ Ошибка передачи ключа шифрования: ${error.message}`, 'encryption_key', 57);
// Продолжаем установку, но предупреждаем пользователя
log.warn('⚠️ Внимание: ключ шифрования не передан. Backend может не запуститься без ключа.');
}
@@ -633,6 +636,16 @@ findtime = 3600
const tempCertCommand = `openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/letsencrypt/live/${domain}/privkey.pem -out /etc/letsencrypt/live/${domain}/fullchain.pem -subj '/C=US/ST=State/L=City/O=Organization/CN=${domain}'`;
await execSshCommand(tempCertCommand, options);
log.success('Временный SSL сертификат создан');
+ // Сообщаем о создании временного сертификата сразу после его генерации,
+ // выставляя прогресс между шагами Docker (55%) и экспортом образов (60%),
+ // чтобы индикатор прогресса не "откатывался" назад.
+ log.info('ℹ️ Временный SSL сертификат создан. Для получения реального SSL сертификата используйте кнопку \"Получить / обновить SSL\" на странице /vds.');
+ sendWebSocketLog(
+ 'info',
+ 'ℹ️ Временный SSL сертификат установлен. Для получения реального SSL нажмите \"Получить / обновить SSL\" в интерфейсе VDS.',
+ 'ssl_cert',
+ 58
+ );
// 12. Передача docker-compose.prod.yml на VDS
log.info('Передача docker-compose.prod.yml на VDS...');
@@ -689,153 +702,24 @@ WS_BACKEND_CONTAINER=dapp-backend`;
log.info('Запуск приложения...');
await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml up -d`, options);
- // 16.1. 🆕 Настройка CORS заголовков в nginx для API
+ // 16.1. Настройка CORS заголовков в nginx для API
log.info('🔧 Настройка CORS заголовков в nginx для API...');
- await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml exec frontend-nginx sed -i '/add_header X-XSS-Protection/a\\ add_header Access-Control-Allow-Origin \"https://${domain}\" always;\\ add_header Access-Control-Allow-Methods \"GET, POST, PUT, DELETE, OPTIONS\" always;\\ add_header Access-Control-Allow-Headers \"Content-Type, Authorization, X-Requested-With\" always;\\ add_header Access-Control-Allow-Credentials \"true\" always;' /etc/nginx/nginx.conf`, options);
+ await execSshCommand(
+ `cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml exec frontend-nginx sed -i '/add_header X-XSS-Protection/a\\ add_header Access-Control-Allow-Origin \"https://${domain}\" always;\\ add_header Access-Control-Allow-Methods \"GET, POST, PUT, DELETE, OPTIONS\" always;\\ add_header Access-Control-Allow-Headers \"Content-Type, Authorization, X-Requested-With\" always;\\ add_header Access-Control-Allow-Credentials \"true\" always;' /etc/nginx/nginx.conf`,
+ options
+ );
// Перезапускаем nginx с новой конфигурацией
await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml restart frontend-nginx`, options);
log.success('✅ CORS заголовки настроены в nginx для API');
- // 16.0. 🆕 Получение реального SSL сертификата через Let's Encrypt
- log.info('🔒 Получение реального SSL сертификата через Let\'s Encrypt...');
- sendWebSocketLog('info', '🔒 Получение SSL сертификата...', 'ssl_cert', 75);
+ // 16.0. Получение реального SSL сертификата перенесено в backend (/api/vds/ssl/renew).
+ // Здесь агент создает только временный самоподписанный сертификат (см. шаг 11 выше).
+ // Для получения/обновления реального сертификата используйте кнопку
+ // "Получить / обновить SSL" на странице управления VDS в интерфейсе DLE,
+ // которая вызывает /api/vds/ssl/renew на backend.
- try {
- // Убеждаемся, что директории для certbot существуют
- log.info('📁 Подготовка директорий для certbot...');
- await execSshCommand('mkdir -p /var/www/certbot/.well-known/acme-challenge', options);
- await execSshCommand('chmod -R 755 /var/www/certbot', options);
-
- // Проверяем, запущен ли frontend-nginx
- log.info('🔍 Проверка статуса frontend-nginx...');
- const nginxStatus = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml ps frontend-nginx --format json 2>/dev/null || echo 'not running'`, options);
-
- let tempHttpContainerStarted = false;
- let nginxRunning = false;
-
- // Проверяем, доступен ли порт 80
- const port80Check = await execSshCommand('netstat -tuln | grep ":80 " || ss -tuln | grep ":80 " || echo "port 80 not listening"', options);
-
- if (port80Check.stdout.includes(':80 ') || port80Check.stdout.includes('LISTEN')) {
- log.info('✅ Порт 80 уже занят, проверяем доступность challenge...');
- const challengeToken = `test-${Date.now()}`;
- await execSshCommand(`echo 'test' > /var/www/certbot/.well-known/acme-challenge/${challengeToken}`, options);
- const challengeCheck = await execSshCommand(`curl -fsS http://${domain}/.well-known/acme-challenge/${challengeToken} 2>&1 || curl -fsS http://localhost/.well-known/acme-challenge/${challengeToken} 2>&1`, options);
- await execSshCommand(`rm -f /var/www/certbot/.well-known/acme-challenge/${challengeToken}`, options);
-
- if (challengeCheck.code === 0) {
- log.success('✅ HTTP challenge доступен через существующий веб-сервер');
- nginxRunning = true;
- } else {
- log.warn('⚠️ HTTP challenge недоступен через существующий сервер');
- }
- }
-
- // Если порт 80 не занят или challenge недоступен, запускаем временный nginx
- if (!nginxRunning) {
- log.info('🚀 Запуск временного nginx для HTTP challenge...');
- await execSshCommand('docker rm -f dle-certbot-http 2>/dev/null || true', options);
-
- // Останавливаем frontend-nginx если он запущен (чтобы освободить порт 80)
- await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml stop frontend-nginx 2>/dev/null || true`, options);
-
- // Запускаем временный nginx для challenge
- const tempNginxStart = await execSshCommand('docker run -d --name dle-certbot-http --network dapp_network -p 80:80 -v /var/www/certbot:/usr/share/nginx/html:ro nginx:alpine 2>&1', options);
-
- if (tempNginxStart.code === 0) {
- tempHttpContainerStarted = true;
- log.success('✅ Временный nginx запущен для HTTP challenge');
- await execSshCommand('sleep 5', options); // Даем время nginx запуститься
-
- // Проверяем доступность challenge
- const challengeToken = `verify-${Date.now()}`;
- await execSshCommand(`echo 'verify' > /var/www/certbot/.well-known/acme-challenge/${challengeToken}`, options);
- const verifyCheck = await execSshCommand(`curl -fsS http://${domain}/.well-known/acme-challenge/${challengeToken} 2>&1 || curl -fsS http://localhost/.well-known/acme-challenge/${challengeToken} 2>&1`, options);
- await execSshCommand(`rm -f /var/www/certbot/.well-known/acme-challenge/${challengeToken}`, options);
-
- if (verifyCheck.code === 0) {
- log.success('✅ HTTP challenge доступен через временный nginx');
- } else {
- log.warn(`⚠️ HTTP challenge недоступен: ${verifyCheck.stderr || verifyCheck.stdout}`);
- }
- } else {
- log.error(`❌ Не удалось запустить временный nginx: ${tempNginxStart.stderr || tempNginxStart.stdout}`);
- throw new Error('Не удалось запустить временный nginx для HTTP challenge');
- }
- }
-
- // Получаем SSL сертификат через certbot
- log.info('📜 Получение SSL сертификата через certbot...');
- const certbotResult = await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml run --rm --no-deps certbot 2>&1`, options);
-
- if (certbotResult.code === 0) {
- log.success('✅ Реальный SSL сертификат успешно получен от Let\'s Encrypt');
- sendWebSocketLog('success', '✅ SSL сертификат получен', 'ssl_cert', 80);
-
- // Проверяем наличие сертификата
- const certCheck = await execSshCommand(`ls -la /etc/letsencrypt/live/${domain}/fullchain.pem /etc/letsencrypt/live/${domain}/privkey.pem 2>&1`, options);
- if (certCheck.code === 0) {
- log.info(`📋 Сертификаты найдены:\n${certCheck.stdout}`);
- }
- } else {
- const errorMsg = certbotResult.stderr || certbotResult.stdout || 'Неизвестная ошибка';
- log.warn(`⚠️ Предупреждение при получении SSL сертификата: ${errorMsg}`);
- log.info('ℹ️ Будет использоваться временный самоподписанный сертификат');
- sendWebSocketLog('warning', `⚠️ SSL сертификат не получен: ${errorMsg.substring(0, 100)}`, 'ssl_cert', 80);
- }
-
- // Останавливаем временный nginx если он был запущен
- if (tempHttpContainerStarted) {
- log.info('🛑 Остановка временного nginx контейнера...');
- await execSshCommand('docker rm -f dle-certbot-http 2>/dev/null || true', options);
-
- // Перезапускаем frontend-nginx если он был остановлен
- log.info('🔄 Перезапуск frontend-nginx...');
- await execSshCommand(`cd /home/${dockerUser}/dapp && docker compose -f docker-compose.prod.yml up -d frontend-nginx 2>&1`, options);
- }
-
- } catch (error) {
- log.error(`❌ Ошибка при получении SSL сертификата: ${error.message}`);
- log.error('📋 Детали ошибки:', error.stack);
- sendWebSocketLog('error', `❌ Ошибка получения SSL сертификата: ${error.message}`, 'ssl_cert', 80);
- log.warn('⚠️ Продолжаем с временным самоподписанным сертификатом');
- }
-
- // Настройка автоматического обновления SSL сертификатов
- log.info('⚙️ Настройка автоматического обновления SSL сертификатов...');
- const renewScript = `#!/bin/bash
-# Автоматическое обновление SSL сертификатов через Docker certbot
-cd /home/${dockerUser}/dapp
-echo "$(date): Проверка обновления SSL сертификатов..." >> /var/log/ssl-renewal.log
-
-# Обновляем сертификаты через certbot (--no-deps чтобы не ждать зависимости)
-docker compose -f docker-compose.prod.yml run --rm --no-deps certbot renew --non-interactive 2>&1 | tee -a /var/log/ssl-renewal.log
-
-if [ $? -eq 0 ]; then
- echo "$(date): SSL сертификаты обновлены, перезапуск nginx..." >> /var/log/ssl-renewal.log
- docker compose -f docker-compose.prod.yml restart frontend-nginx 2>&1 | tee -a /var/log/ssl-renewal.log
-else
- echo "$(date): Ошибка обновления SSL сертификатов" >> /var/log/ssl-renewal.log
-fi
-`;
- await execSshCommand(`cat > /home/${dockerUser}/dapp/renew-ssl.sh << 'RENEW_EOF'
-${renewScript}
-RENEW_EOF
-`, options);
- await execSshCommand(`chmod +x /home/${dockerUser}/dapp/renew-ssl.sh`, options);
-
- // Устанавливаем cron задачу (если crontab доступен)
- const cronCheck = await execSshCommand('which crontab 2>/dev/null || echo "crontab not found"', options);
- if (cronCheck.stdout.includes('crontab')) {
- await execSshCommand(`(crontab -l 2>/dev/null | grep -v renew-ssl.sh; echo "0 12 * * * /home/${dockerUser}/dapp/renew-ssl.sh") | crontab -`, options);
- log.success('✅ Автоматическое обновление SSL сертификатов настроено (ежедневно в 12:00)');
- } else {
- log.warn('⚠️ crontab не найден, автоматическое обновление не настроено');
- log.info('💡 Для ручного обновления выполните: /home/${dockerUser}/dapp/renew-ssl.sh');
- }
-
- // 16.1. 🆕 Ожидание готовности базы данных с повторными попытками
+ // 16.2. Ожидание готовности базы данных с повторными попытками
log.info('Ожидание готовности базы данных...');
let dbReady = false;
let attempts = 0;
diff --git a/webssh-agent/utils/cleanupUtils.js b/webssh-agent/utils/cleanupUtils.js
index 0ba3749..b42d2e1 100644
--- a/webssh-agent/utils/cleanupUtils.js
+++ b/webssh-agent/utils/cleanupUtils.js
@@ -163,13 +163,26 @@ const setupRootSshKeys = async (publicKey, options) => {
// Создание директории .ssh для root
await execSshCommand('mkdir -p /root/.ssh', options);
await execSshCommand('chmod 700 /root/.ssh', options);
+ // ВАЖНО: Устанавливаем правильного владельца директории (root:root)
+ // SSH не принимает ключи, если директория принадлежит другому пользователю
+ await execSshCommand('chown root:root /root/.ssh', options);
// Добавление публичного ключа в authorized_keys
- await execSshCommand(`echo "${publicKey}" >> /root/.ssh/authorized_keys`, options);
+ // Используем printf для безопасной обработки специальных символов в ключе
+ // Экранируем обратные слеши и знаки доллара в публичном ключе
+ const escapedPublicKey = publicKey.replace(/\\/g, '\\\\').replace(/\$/g, '\\$');
+ await execSshCommand(`printf '%s\\n' "${escapedPublicKey}" >> /root/.ssh/authorized_keys`, options);
await execSshCommand('chmod 600 /root/.ssh/authorized_keys', options);
await execSshCommand('chown root:root /root/.ssh/authorized_keys', options);
- log.success('SSH ключи созданы и публичный ключ добавлен в authorized_keys');
+ // Проверяем, что ключ действительно добавлен
+ const verifyResult = await execSshCommand(`grep -Fx "${escapedPublicKey}" /root/.ssh/authorized_keys > /dev/null && echo "OK" || echo "FAIL"`, options);
+ if (verifyResult.stdout.trim() === 'OK') {
+ log.success('SSH ключи созданы и публичный ключ добавлен в authorized_keys');
+ } else {
+ log.error('Ошибка: публичный ключ не был добавлен в authorized_keys');
+ throw new Error('Не удалось добавить публичный ключ в authorized_keys');
+ }
};
/**
diff --git a/webssh-agent/utils/dockerUtils.js b/webssh-agent/utils/dockerUtils.js
index b833625..88c846f 100644
--- a/webssh-agent/utils/dockerUtils.js
+++ b/webssh-agent/utils/dockerUtils.js
@@ -50,6 +50,9 @@ const exportDockerImages = async (sendWebSocketLog) => {
{ name: 'digital_legal_entitydle-webssh-agent:latest', file: 'dapp-webssh-agent.tar' }
];
+ // Список реально экспортированных файлов образов
+ const exportedImageFiles = [];
+
// Экспортируем все образы
for (let i = 0; i < images.length; i++) {
const image = images[i];
@@ -59,8 +62,20 @@ const exportDockerImages = async (sendWebSocketLog) => {
try {
const outputPath = `/tmp/${image.file}`;
+ // Проверяем, существует ли образ локально
+ const inspectResult = await execLocalCommand(`docker images -q ${image.name} || true`);
+ const imageId = inspectResult.stdout.trim();
+
+ if (!imageId) {
+ const msg = `Образ ${image.name} не найден локально, пропускаем экспорт`;
+ log.warn(msg);
+ sendWebSocketLog('warning', `⚠️ ${msg}`, 'export_images', progress);
+ continue;
+ }
+
// Безопасный экспорт через CLI
- await execDockerCommand(`docker save ${image.name} > ${outputPath}`);
+ await execDockerCommand(`docker save -o ${outputPath} ${image.name}`);
+ exportedImageFiles.push(image.file);
sendWebSocketLog('success', `✅ Экспорт ${image.name} завершен`, 'export_images', progress);
} catch (error) {
@@ -95,12 +110,14 @@ const exportDockerImages = async (sendWebSocketLog) => {
sendWebSocketLog('info', '📦 Создание архива всех данных...', 'export_data', 80);
try {
- const tarFiles = images.map(img => img.file).join(' ');
+ const tarFiles = exportedImageFiles.join(' ');
// Динамически собираем список файлов данных из экспортированных volumes
const dataFilesList = await execLocalCommand('ls /tmp/*_data.tar.gz 2>/dev/null | xargs -r basename -a || echo ""');
const dataFiles = dataFilesList.stdout.trim().split('\n').filter(f => f).join(' ');
- const archiveCommand = `cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles || ''}`.trim();
+ const archiveCommand = tarFiles
+ ? `cd /tmp && tar -czf docker-images-and-data.tar.gz ${tarFiles} ${dataFiles || ''}`.trim()
+ : `cd /tmp && tar -czf docker-images-and-data.tar.gz ${dataFiles || ''}`.trim();
await execLocalCommand(archiveCommand);
sendWebSocketLog('success', '✅ Архив создан успешно', 'export_data', 80);
@@ -160,7 +177,6 @@ const importDockerImages = async (options, sendWebSocketLog) => {
sendWebSocketLog('info', '📥 Начинаем импорт данных на VDS...', 'import', 85);
const importScript = `#!/bin/bash
-set -e
echo "🚀 Импорт Docker образов и данных на VDS..."
# Проверяем наличие архива
@@ -180,8 +196,17 @@ tar -xzf ./docker-images-and-data.tar.gz -C ./temp-import
echo "📦 Импорт образов..."
for image_file in ./temp-import/dapp-*.tar; do
if [ -f "$image_file" ]; then
- echo "📦 Импорт образа: $(basename $image_file)"
- docker load -i "$image_file"
+ # Пропускаем пустые или поврежденные файлы образов
+ if [ ! -s "$image_file" ]; then
+ echo "⚠️ Пропуск пустого файла образа: $(basename "$image_file")"
+ continue
+ fi
+
+ echo "📦 Импорт образа: $(basename "$image_file")"
+ if ! docker load -i "$image_file"; then
+ echo "❌ Ошибка импорта образа: $(basename "$image_file"), продолжаем со следующими"
+ continue
+ fi
fi
done
@@ -198,14 +223,14 @@ for data_file in ./temp-import/*_data.tar.gz; do
volume_name=$(basename "$data_file" .tar.gz 2>/dev/null || echo "")
# Проверяем, что volume_name не пустой и не содержит только пробелы
- if [ -z "${volume_name:-}" ] || [ -z "$(echo "${volume_name}" | tr -d '[:space:]')" ]; then
+ if [ -z "$volume_name" ] || [ -z "$(echo "$volume_name" | tr -d '[:space:]')" ]; then
echo "⚠️ Предупреждение: не удалось извлечь имя volume из файла: $data_file"
volume_name=""
continue
fi
# Используем префикс dapp_ для соответствия docker-compose.prod.yml
- full_volume_name="dapp_${volume_name}"
+ full_volume_name="dapp_$volume_name"
echo "📦 Импорт данных: $full_volume_name"
# Удаляем старый volume если существует
diff --git a/webssh-agent/utils/userUtils.js b/webssh-agent/utils/userUtils.js
index 3bdcf39..572e409 100644
--- a/webssh-agent/utils/userUtils.js
+++ b/webssh-agent/utils/userUtils.js
@@ -13,7 +13,10 @@ const createUserWithSshKeys = async (username, publicKey, options) => {
// Настройка SSH ключей для пользователя
await execSshCommand(`mkdir -p /home/${username}/.ssh`, options);
- await execSshCommand(`echo "${publicKey}" > /home/${username}/.ssh/authorized_keys`, options);
+ // Используем printf для безопасной обработки специальных символов в ключе
+ // Экранируем обратные слеши и знаки доллара в публичном ключе
+ const escapedPublicKey = publicKey.replace(/\\/g, '\\\\').replace(/\$/g, '\\$');
+ await execSshCommand(`printf '%s\\n' "${escapedPublicKey}" > /home/${username}/.ssh/authorized_keys`, options);
await execSshCommand(`chown -R ${username}:${username} /home/${username}/.ssh`, options);
await execSshCommand(`chmod 700 /home/${username}/.ssh`, options);
await execSshCommand(`chmod 600 /home/${username}/.ssh/authorized_keys`, options);