Files
links/scripts/master-deploy.sh
Andrey K. Choi 280a6c4ad0
Some checks failed
continuous-integration/drone/push Build is failing
🔧 Исправлена поддержка Docker Compose v2
 Fixes:
- Добавлена поддержка Docker Compose v2 (docker compose)
- Автоматическое определение версии Docker Compose в Makefile
- Обновлены скрипты master-deploy.sh и pre-deploy-check.sh
- Добавлена утилита detect-docker-compose.sh
- Исправлены команды в функции setup_database

🔧 Изменения:
- Makefile: улучшенное определение команды Docker Compose
- master-deploy.sh: использует make команды вместо прямых вызовов
- pre-deploy-check.sh: проверка обеих версий Docker Compose
- Добавлена команда shell-exec в Makefile

Теперь скрипт корректно работает с Docker Compose v2 на современных системах.
2025-11-04 20:12:09 +09:00

722 lines
25 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# CatLink Master Deployment Script
# Полное автоматическое развертывание проекта с генерацией всех ключей и настроек
set -e
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Логирование
log() {
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"
}
success() {
echo -e "${GREEN}$1${NC}"
}
warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
error() {
echo -e "${RED}$1${NC}"
exit 1
}
# Функция для генерации случайных паролей
generate_password() {
local length=${1:-32}
openssl rand -base64 $length | tr -d "=+/" | cut -c1-$length
}
# Функция для генерации Django secret key
generate_django_secret() {
python3 -c "
import secrets
import string
chars = string.ascii_letters + string.digits + '!@#$%^&*(-_=+)'
print(''.join(secrets.choice(chars) for i in range(50)))
"
}
# Глобальная переменная для команды Docker Compose
DOCKER_COMPOSE_CMD=""
# Проверка требований
check_requirements() {
log "🔍 Проверка системных требований..."
# Проверяем Docker
if ! command -v docker &> /dev/null; then
error "Docker не установлен. Установите Docker и попробуйте снова."
fi
# Проверяем Docker Compose (v1 или v2)
DOCKER_COMPOSE_CMD=$(./scripts/detect-docker-compose.sh)
if [[ -z "$DOCKER_COMPOSE_CMD" ]]; then
error "Docker Compose не установлен. Установите Docker Compose и попробуйте снова."
fi
if [[ "$DOCKER_COMPOSE_CMD" == "docker-compose" ]]; then
log "✅ Найден Docker Compose v1"
else
log "✅ Найден Docker Compose v2"
fi
# Проверяем nginx
if ! command -v nginx &> /dev/null; then
warning "nginx не установлен. Установка nginx..."
sudo apt update
sudo apt install -y nginx
fi
# Проверяем certbot
if ! command -v certbot &> /dev/null; then
warning "certbot не установлен. Установка certbot..."
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
fi
# Проверяем openssl
if ! command -v openssl &> /dev/null; then
sudo apt install -y openssl
fi
# Проверяем python3
if ! command -v python3 &> /dev/null; then
sudo apt install -y python3
fi
success "Все системные требования выполнены"
}
# Сбор информации от пользователя
collect_deployment_info() {
log "📋 Сбор информации для развертывания..."
# Домен
while true; do
read -p "🌐 Введите основной домен (например: links.shareon.kr): " MAIN_DOMAIN
if [[ $MAIN_DOMAIN =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
break
else
warning "Неверный формат домена. Попробуйте снова."
fi
done
# Дополнительные домены
read -p "🌐 Введите дополнительные домены через запятую (или Enter для пропуска): " ADDITIONAL_DOMAINS
# Email для Let's Encrypt
while true; do
read -p "📧 Введите email для Let's Encrypt: " LETSENCRYPT_EMAIL
if [[ $LETSENCRYPT_EMAIL =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
break
else
warning "Неверный формат email. Попробуйте снова."
fi
done
# Режим SSL
echo "🔒 Выберите режим SSL:"
echo "1) Автоматически получить Let's Encrypt сертификаты"
echo "2) Использовать самоподписанные сертификаты (для тестирования)"
echo "3) Пропустить SSL (только HTTP)"
read -p "Выберите опцию (1-3): " SSL_MODE
# Окружение
echo "🏷️ Выберите окружение:"
echo "1) production (рекомендуется для живого сайта)"
echo "2) staging (для тестирования)"
echo "3) development (для разработки)"
read -p "Выберите опцию (1-3): " ENV_MODE
case $ENV_MODE in
1) ENVIRONMENT="production" ;;
2) ENVIRONMENT="staging" ;;
3) ENVIRONMENT="development" ;;
*) ENVIRONMENT="production" ;;
esac
success "Информация собрана"
}
# Генерация .env файла
generate_env_file() {
log "⚙️ Генерация .env файла с безопасными настройками..."
# Генерируем пароли
DJANGO_SECRET_KEY=$(generate_django_secret)
DB_PASSWORD=$(generate_password 32)
# Формируем список доменов
if [[ -n "$ADDITIONAL_DOMAINS" ]]; then
ALL_DOMAINS="$MAIN_DOMAIN,$ADDITIONAL_DOMAINS,localhost,127.0.0.1"
else
ALL_DOMAINS="$MAIN_DOMAIN,localhost,127.0.0.1"
fi
# Определяем API URL
if [[ "$SSL_MODE" == "1" ]] || [[ "$SSL_MODE" == "2" ]]; then
API_URL="https://$MAIN_DOMAIN"
else
API_URL="http://$MAIN_DOMAIN"
fi
# Создаем .env файл
cat > .env << EOF
# =============================================================================
# CatLink Configuration - Generated $(date)
# Environment: $ENVIRONMENT
# =============================================================================
# Django настройки
DJANGO_SECRET_KEY=$DJANGO_SECRET_KEY
DJANGO_DEBUG=$([ "$ENVIRONMENT" = "development" ] && echo "True" || echo "False")
DJANGO_ALLOWED_HOSTS=$ALL_DOMAINS
# База данных PostgreSQL
DATABASE_ENGINE=django.db.backends.postgresql
DATABASE_NAME=links_db
DATABASE_USER=links_user
DATABASE_PASSWORD=$DB_PASSWORD
DATABASE_HOST=db
DATABASE_PORT=5432
# PostgreSQL настройки для контейнера
POSTGRES_DB=links_db
POSTGRES_USER=links_user
POSTGRES_PASSWORD=$DB_PASSWORD
# Frontend настройки
NEXT_PUBLIC_API_URL=$API_URL
# SSL/Security настройки
DJANGO_SECURE_SSL_REDIRECT=$([ "$SSL_MODE" = "1" ] && echo "True" || echo "False")
DJANGO_SECURE_HSTS_SECONDS=$([ "$SSL_MODE" = "1" ] && echo "31536000" || echo "0")
DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=$([ "$SSL_MODE" = "1" ] && echo "True" || echo "False")
DJANGO_SECURE_HSTS_PRELOAD=$([ "$SSL_MODE" = "1" ] && echo "True" || echo "False")
# Let's Encrypt настройки
LETSENCRYPT_EMAIL=$LETSENCRYPT_EMAIL
MAIN_DOMAIN=$MAIN_DOMAIN
ADDITIONAL_DOMAINS=$ADDITIONAL_DOMAINS
# Environment
ENVIRONMENT=$ENVIRONMENT
# Session/Cookie настройки
DJANGO_SESSION_COOKIE_SECURE=$([ "$SSL_MODE" = "1" ] && echo "True" || echo "False")
DJANGO_CSRF_COOKIE_SECURE=$([ "$SSL_MODE" = "1" ] && echo "True" || echo "False")
# Email настройки (опционально - настройте при необходимости)
# DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
# DJANGO_EMAIL_HOST=smtp.gmail.com
# DJANGO_EMAIL_PORT=587
# DJANGO_EMAIL_HOST_USER=your-email@gmail.com
# DJANGO_EMAIL_HOST_PASSWORD=your-app-password
# DJANGO_EMAIL_USE_TLS=True
# DJANGO_DEFAULT_FROM_EMAIL=noreply@$MAIN_DOMAIN
# Backup настройки
BACKUP_SCHEDULE="0 2 * * *" # Ежедневно в 2:00
BACKUP_RETENTION_DAYS=30
EOF
success ".env файл создан с безопасными настройками"
# Создаем backup .env файла
cp .env ".env.backup.$(date +%Y%m%d_%H%M%S)"
log "📦 Backup .env файла создан"
}
# Настройка nginx
setup_nginx() {
log "🌐 Настройка nginx..."
# Создаем конфигурацию nginx
sudo tee /etc/nginx/sites-available/catlink << EOF
# CatLink nginx configuration
# Generated $(date)
server {
listen 80;
server_name $MAIN_DOMAIN${ADDITIONAL_DOMAINS:+ $ADDITIONAL_DOMAINS};
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_cache_bypass \$http_upgrade;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location /admin/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
location /static/ {
proxy_pass http://127.0.0.1:8000;
expires 1y;
add_header Cache-Control "public, immutable";
}
location /media/ {
proxy_pass http://127.0.0.1:8000;
expires 1y;
add_header Cache-Control "public";
}
# Security
location ~ /\\.ht {
deny all;
}
location = /robots.txt {
proxy_pass http://127.0.0.1:8000;
access_log off;
}
location = /favicon.ico {
proxy_pass http://127.0.0.1:3000;
access_log off;
}
}
EOF
# Включаем сайт
sudo ln -sf /etc/nginx/sites-available/catlink /etc/nginx/sites-enabled/
# Удаляем дефолтный сайт
sudo rm -f /etc/nginx/sites-enabled/default
# Проверяем конфигурацию
if sudo nginx -t; then
success "Конфигурация nginx корректна"
sudo systemctl reload nginx
else
error "Ошибка в конфигурации nginx"
fi
}
# Настройка SSL
setup_ssl() {
if [[ "$SSL_MODE" == "1" ]]; then
log "🔒 Получение Let's Encrypt сертификатов..."
# Формируем список доменов для certbot
CERT_DOMAINS="-d $MAIN_DOMAIN"
if [[ -n "$ADDITIONAL_DOMAINS" ]]; then
IFS=',' read -ra DOMAINS <<< "$ADDITIONAL_DOMAINS"
for domain in "${DOMAINS[@]}"; do
domain=$(echo $domain | xargs) # trim whitespace
CERT_DOMAINS="$CERT_DOMAINS -d $domain"
done
fi
# Получаем сертификаты
sudo certbot --nginx $CERT_DOMAINS --email $LETSENCRYPT_EMAIL --agree-tos --non-interactive --redirect
if [[ $? -eq 0 ]]; then
success "SSL сертификаты получены успешно"
# Настраиваем автообновление
echo "0 12 * * * /usr/bin/certbot renew --quiet" | sudo crontab -
log "📅 Настроено автообновление SSL сертификатов"
else
warning "Не удалось получить SSL сертификаты. Продолжаем без SSL."
SSL_MODE="3"
fi
elif [[ "$SSL_MODE" == "2" ]]; then
log "🔒 Создание самоподписанных SSL сертификатов..."
# Создаем директорию для сертификатов
sudo mkdir -p /etc/ssl/private /etc/ssl/certs
# Генерируем самоподписанный сертификат
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/$MAIN_DOMAIN.key \
-out /etc/ssl/certs/$MAIN_DOMAIN.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=$MAIN_DOMAIN"
# Обновляем nginx конфигурацию для SSL
sudo tee -a /etc/nginx/sites-available/catlink << EOF
server {
listen 443 ssl http2;
server_name $MAIN_DOMAIN${ADDITIONAL_DOMAINS:+ $ADDITIONAL_DOMAINS};
ssl_certificate /etc/ssl/certs/$MAIN_DOMAIN.crt;
ssl_certificate_key /etc/ssl/private/$MAIN_DOMAIN.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Redirect HTTP to HTTPS
if (\$scheme != "https") {
return 301 https://\$host\$request_uri;
}
# Include all location blocks from HTTP server
include /etc/nginx/snippets/catlink-locations.conf;
}
EOF
# Создаем общие location блоки
sudo tee /etc/nginx/snippets/catlink-locations.conf << 'EOF'
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
EOF
sudo systemctl reload nginx
success "Самоподписанные SSL сертификаты созданы"
else
log "🔓 SSL пропущен - использование только HTTP"
fi
}
# Подготовка базы данных
setup_database() {
log "🗄️ Настройка и проверка безопасности базы данных..."
# Ждем запуска контейнеров
log "⏳ Ожидание запуска контейнеров..."
sleep 15
# Проверяем статус контейнеров
if ! make status | grep -q "Up"; then
warning "Контейнеры могут быть не готовы, ожидаем еще..."
sleep 10
fi
# Применяем миграции
log "🔄 Применение миграций базы данных..."
if ! make migrate; then
error "Ошибка применения миграций"
fi
# Создаем суперпользователя
log "👤 Настройка суперпользователя..."
cat > /tmp/create_superuser.py << 'PYTHON_EOF'
from django.contrib.auth import get_user_model
User = get_user_model()
if not User.objects.filter(username='admin').exists():
User.objects.create_superuser('admin', 'admin@example.com', 'admin123')
print('✅ Суперпользователь admin создан с паролем admin123')
print('⚠️ ВАЖНО: Смените пароль после первого входа!')
else:
print(' Суперпользователь уже существует')
PYTHON_EOF
# Выполняем скрипт создания суперпользователя
if make shell-exec CMD="python manage.py shell < /tmp/create_superuser.py" 2>/dev/null; then
success "Суперпользователь настроен"
else
# Fallback - используем прямую команду
$(./scripts/detect-docker-compose.sh) exec web python manage.py shell < /tmp/create_superuser.py || warning "Не удалось создать суперпользователя автоматически"
fi
# Удаляем временный файл
rm -f /tmp/create_superuser.py
from django.contrib.auth import get_user_model
User = get_user_model()
if not User.objects.filter(username='admin').exists():
User.objects.create_superuser('admin', 'admin@example.com', 'admin123')
print("Суперпользователь 'admin' создан с паролем 'admin123'")
print("ВАЖНО: Смените пароль после первого входа!")
else:
print("Суперпользователь уже существует")
EOF
# Запускаем аудит безопасности БД
if [[ -f "./scripts/audit-db-security.sh" ]]; then
log "🔍 Запуск аудита безопасности базы данных..."
./scripts/audit-db-security.sh
fi
# Применяем настройки безопасности БД
if [[ -f "./scripts/setup-db-security.sh" ]]; then
log "🔒 Применение настроек безопасности базы данных..."
./scripts/setup-db-security.sh
fi
success "База данных настроена и защищена"
}
# Создание backup системы
setup_backup_system() {
log "💾 Настройка системы резервного копирования..."
# Создаем директории для backup
mkdir -p backups/{database,files,configs}
# Создаем скрипт backup
cat > scripts/auto-backup.sh << 'EOF'
#!/bin/bash
# Автоматический backup CatLink
set -e
BACKUP_DIR="/opt/links/backups"
DATE=$(date +%Y%m%d_%H%M%S)
# Database backup
docker exec links-db-1 pg_dump -U postgres links_db | gzip > "$BACKUP_DIR/database/links_db_$DATE.sql.gz"
# Files backup
tar -czf "$BACKUP_DIR/files/media_$DATE.tar.gz" backend/storage/
# Config backup
cp .env "$BACKUP_DIR/configs/env_$DATE"
cp docker-compose.yml "$BACKUP_DIR/configs/docker-compose_$DATE.yml"
# Cleanup old backups (keep 30 days)
find "$BACKUP_DIR" -name "*.gz" -mtime +30 -delete
find "$BACKUP_DIR" -name "*.sql" -mtime +30 -delete
echo "Backup completed: $DATE"
EOF
chmod +x scripts/auto-backup.sh
# Добавляем в crontab
(crontab -l 2>/dev/null || true; echo "0 2 * * * cd /opt/links && ./scripts/auto-backup.sh >> logs/backup.log 2>&1") | crontab -
success "Система backup настроена (ежедневно в 2:00)"
}
# Создание мониторинга
setup_monitoring() {
log "📊 Настройка базового мониторинга..."
# Создаем скрипт мониторинга
cat > scripts/health-check.sh << 'EOF'
#!/bin/bash
# Health check для CatLink
check_service() {
if docker-compose ps | grep -q "$1.*Up"; then
echo "✅ $1: OK"
return 0
else
echo "❌ $1: FAILED"
return 1
fi
}
echo "🏥 CatLink Health Check - $(date)"
echo "================================"
check_service "web"
check_service "db"
check_service "frontend"
echo ""
echo "🌐 URL checks:"
curl -s -o /dev/null -w "Frontend: %{http_code}\n" http://localhost:3000/ || echo "Frontend: FAILED"
curl -s -o /dev/null -w "Backend: %{http_code}\n" http://localhost:8000/api/ || echo "Backend: FAILED"
echo ""
echo "💾 Disk usage:"
df -h | grep -E "/(|opt|var)"
echo ""
echo "🐳 Docker resources:"
docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}"
EOF
chmod +x scripts/health-check.sh
success "Мониторинг настроен (./scripts/health-check.sh)"
}
# Финальная проверка
final_verification() {
log "🔍 Финальная проверка развертывания..."
# Проверяем контейнеры
if ! docker-compose ps | grep -q "Up"; then
error "Контейнеры не запущены"
fi
# Проверяем nginx
if ! sudo systemctl is-active --quiet nginx; then
error "nginx не запущен"
fi
# Проверяем доступность сервисов
sleep 10
if curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/ | grep -q "200"; then
success "Frontend доступен"
else
warning "Frontend может быть недоступен"
fi
if curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/api/ | grep -q "200\|404"; then
success "Backend доступен"
else
warning "Backend может быть недоступен"
fi
success "Развертывание завершено успешно!"
}
# Отображение итогов
show_deployment_summary() {
echo ""
echo "🎉 ===== РАЗВЕРТЫВАНИЕ ЗАВЕРШЕНО ===== 🎉"
echo ""
echo "📋 Информация о развертывании:"
echo " 🌐 Основной домен: $MAIN_DOMAIN"
echo " 🔒 SSL режим: $([ "$SSL_MODE" = "1" ] && echo "Let's Encrypt" || [ "$SSL_MODE" = "2" ] && echo "Self-signed" || echo "Disabled")"
echo " 🏷️ Окружение: $ENVIRONMENT"
echo " 📧 Email: $LETSENCRYPT_EMAIL"
echo ""
echo "🔗 Ссылки:"
if [[ "$SSL_MODE" == "1" ]] || [[ "$SSL_MODE" == "2" ]]; then
echo " 🏠 Сайт: https://$MAIN_DOMAIN"
echo " 🔧 Админка: https://$MAIN_DOMAIN/admin/"
else
echo " 🏠 Сайт: http://$MAIN_DOMAIN"
echo " 🔧 Админка: http://$MAIN_DOMAIN/admin/"
fi
echo ""
echo "👤 Учетные данные:"
echo " 👨‍💼 Админ: admin / admin123 (СМЕНИТЕ ПАРОЛЬ!)"
echo ""
echo "📂 Важные файлы:"
echo " ⚙️ Конфигурация: .env"
echo " 💾 Backup: backups/"
echo " 📊 Логи: logs/"
echo ""
echo "🛠️ Полезные команды:"
echo " 📊 Проверка здоровья: ./scripts/health-check.sh"
echo " 💾 Backup: ./scripts/auto-backup.sh"
echo " 🔍 Аудит БД: make security-audit"
echo " 📝 Логи: make logs"
echo " 🔄 Перезапуск: make restart"
echo ""
echo "🆘 Поддержка:"
echo " 📖 Документация: README.md"
echo " 🔒 Безопасность: SECURITY.md"
echo ""
success "Проект готов к использованию!"
}
# Основная функция
main() {
echo "🚀 ========================================"
echo "🚀 CatLink Master Deployment Script "
echo "🚀 ========================================"
echo ""
# Проверяем что мы в правильной директории
if [ ! -f "docker-compose.yml" ]; then
error "Файл docker-compose.yml не найден. Запустите скрипт из корня проекта."
fi
# Создаем необходимые директории
mkdir -p {logs,backups,scripts}
# Основной процесс развертывания
check_requirements
collect_deployment_info
generate_env_file
setup_nginx
log "🏗️ Сборка и запуск контейнеров..."
# Используем Makefile команды которые автоматически определяют Docker Compose
if ! make build-prod; then
error "Ошибка сборки контейнеров"
fi
if ! make up-prod; then
error "Ошибка запуска контейнеров"
fi
setup_database
setup_ssl
setup_backup_system
setup_monitoring
final_verification
show_deployment_summary
echo ""
echo "🎊 Развертывание CatLink завершено успешно!"
}
# Обработка сигналов
trap 'error "Развертывание прервано пользователем"' INT TERM
# Запуск
main "$@"