@@ -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 ;