diff --git a/AUTO-BACKUP-SETUP.md b/AUTO-BACKUP-SETUP.md new file mode 100644 index 0000000..3653d88 --- /dev/null +++ b/AUTO-BACKUP-SETUP.md @@ -0,0 +1,93 @@ +# Настройка автоматического резервного копирования БД + +## 🎯 Автоматический бэкап дважды в день + +Система автоматически создаёт бэкапы PROD и DEV БД: +- **00:00** (полночь) +- **12:00** (полдень) + +## 📋 Установка + +```bash +cd /var/www/platform/prod + +# Сделать скрипты исполняемыми +chmod +x backup-db-auto.sh setup-cron-backup.sh remove-cron-backup.sh + +# Настроить автоматический бэкап +./setup-cron-backup.sh +``` + +## ✅ Проверка + +```bash +# Проверить, что задача добавлена в cron +crontab -l | grep backup-db-auto + +# Должно быть: +# 0 0,12 * * * PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /var/www/platform/prod/backup-db-auto.sh >> /var/www/platform/prod/backups/cron.log 2>&1 +``` + +## 📊 Логи + +```bash +# Логи автоматических бэкапов +tail -f /var/www/platform/prod/backups/backup.log + +# Логи cron (ошибки выполнения) +tail -f /var/www/platform/prod/backups/cron.log +``` + +## 🗂️ Хранение бэкапов + +- **Директория**: `/var/www/platform/prod/backups/` +- **Формат файлов**: `platform_prod_db_YYYYMMDD_HHMMSS.sql.gz` +- **Автоочистка**: Бэкапы старше 30 дней удаляются автоматически +- **Проверка места**: При использовании диска > 80% в лог пишется предупреждение + +## 🔄 Ручной запуск + +```bash +# Запустить бэкап вручную (для тестирования) +/var/www/platform/prod/backup-db-auto.sh +``` + +## 🗑️ Удаление автоматического бэкапа + +```bash +# Удалить задачу из cron +./remove-cron-backup.sh + +# Или вручную +crontab -l | grep -v backup-db-auto | crontab - +``` + +## 📝 Что делает скрипт + +1. ✅ Проверяет, что контейнеры БД запущены +2. ✅ Создаёт бэкапы PROD и DEV БД +3. ✅ Сжимает бэкапы (gzip) +4. ✅ Проверяет размер бэкапов +5. ✅ Удаляет бэкапы старше 30 дней +6. ✅ Логирует все действия +7. ✅ Предупреждает о нехватке места на диске + +## ⚠️ Важно + +- Скрипт работает от пользователя `root` (нужен доступ к Docker) +- Бэкапы сохраняются в `/var/www/platform/prod/backups/` +- Старые бэкапы (30+ дней) удаляются автоматически +- При ошибках информация записывается в лог + +## 🔍 Мониторинг + +```bash +# Посмотреть последние бэкапы +ls -lh /var/www/platform/prod/backups/*.sql.gz | tail -10 + +# Проверить размер всех бэкапов +du -sh /var/www/platform/prod/backups/ + +# Посмотреть последние записи в логе +tail -20 /var/www/platform/prod/backups/backup.log +``` diff --git a/README-PROD.md b/README-PROD.md new file mode 100644 index 0000000..6490af9 --- /dev/null +++ b/README-PROD.md @@ -0,0 +1,148 @@ +# PROD Окружение - Инструкция по управлению + +## ⚠️ ВАЖНО: Защита данных + +**PROD окружение использует отдельную сеть (`prod_network`) и именованные volumes для изоляции от dev.** + +### Что было исправлено: + +1. ✅ **Отдельная сеть** - `prod_network` вместо общей `dev_network` +2. ✅ **Именованные volumes** - все volumes имеют префикс `platform_prod_` +3. ✅ **Полные имена контейнеров** - в `DATABASE_URL` и `REDIS_URL` используются полные имена контейнеров +4. ✅ **Изоляция от dev** - prod не может случайно подключиться к dev БД + +## 📋 Основные команды + +### Безопасная остановка (СОХРАНЯЕТ данные БД): +```bash +# Использовать скрипт из /var/www/service +/var/www/service/platform/safe-down-prod.sh + +# Или вручную +cd /var/www/platform/prod +docker compose down +``` + +### ⚠️ ОСТОРОЖНО: Полная очистка (УДАЛЯЕТ данные БД): +```bash +# Сначала создайте бэкап! +/var/www/service/backup/backup-prod-db.sh + +# Затем можно удалить volumes +cd /var/www/platform/prod +docker compose down --volumes +``` + +### Запуск: +```bash +docker compose up -d +``` + +### Создание бэкапа БД: +```bash +# Бэкап PROD БД +/var/www/service/backup/backup-all-db.sh + +# Альтернативный скрипт для PROD БД +/var/www/service/backup/backup-prod-db.sh + +# Автоматический бэкап PROD БД (для cron) +/var/www/service/backup/backup-db-auto.sh +``` + +**Примечание:** DEV БД не бэкапится, так как это окружение разработки. + +### Настройка автоматического бэкапа PROD БД (2 раза в день: 00:00 и 12:00): +```bash +# Установить автоматический бэкап PROD БД +/var/www/service/backup/setup-cron-backup.sh + +# Удалить автоматический бэкап +/var/www/service/backup/remove-cron-backup.sh + +# Проверить расписание +crontab -l | grep backup-db-auto + +# Просмотр логов +tail -f /var/www/platform/prod/backups/backup.log +``` + +**Примечание:** Автоматически бэкапится только PROD БД. DEV БД не бэкапится. + +### Полная пересборка PROD (с бэкапом): +```bash +/var/www/service/platform/rebuild-prod.sh +``` +Этот скрипт: +1. Создаёт бэкап БД +2. Останавливает контейнеры +3. Пересобирает образы без кэша +4. Запускает контейнеры +5. Применяет миграции + +### Применение миграций: +```bash +docker exec platform_prod_web python manage.py migrate +``` + +### Создание суперпользователя: +```bash +docker exec -it platform_prod_web python manage.py createsuperuser +``` + +## 🔧 Структура volumes + +- `platform_prod_postgres_data` - данные PostgreSQL БД +- `platform_prod_redis_data` - данные Redis +- `platform_prod_front_material_node_modules` - node_modules для frontend +- `platform_prod_front_material_next` - кэш Next.js + +## 🌐 Сеть + +- **Prod сеть**: `platform_prod_network` (изолирована от dev) +- **Dev сеть**: `dev_network` (отдельная) + +## 🔗 Подключения + +Все сервисы используют полные имена контейнеров: +- БД: `platform_prod_db` (не `db`) +- Redis: `platform_prod_redis` (не `redis`) + +Это гарантирует, что даже при запуске dev и prod одновременно, они не будут конфликтовать. + +## 📝 Что делать если данные потеряны + +1. Проверьте бэкапы: `ls -la ./backups/` +2. Если бэкапа нет, но данные есть в dev БД, можно скопировать: + ```bash + # Создать бэкап из dev + docker exec platform_dev_db pg_dump -U platform_dev_user -d platform_dev_db > /tmp/dev_backup.sql + + # Применить миграции в prod + docker exec platform_prod_web python manage.py migrate + + # Восстановить данные (осторожно!) + docker exec -i platform_prod_db psql -U platform_prod_user -d platform_prod_db < /tmp/dev_backup.sql + ``` +3. Если данных нет нигде - создайте пользователей заново через `createsuperuser` + +## 🚨 Частые ошибки + +### ❌ НЕ делайте: +- `docker compose down --volumes` без бэкапа +- Использование коротких имен (`db`, `redis`) в переменных окружения +- Общая сеть для dev и prod + +### ✅ Делайте: +- Всегда используйте `docker compose down` (без `--volumes`) +- Регулярно создавайте бэкапы PROD БД: `/var/www/service/backup/backup-all-db.sh` +- Используйте полные имена контейнеров в конфигурации + +## 📁 Расположение скриптов + +Все служебные скрипты перенесены в `/var/www/service/`: + +- **Бэкапы**: `/var/www/service/backup/` +- **Управление платформой**: `/var/www/service/platform/` + +Подробнее: `/var/www/service/README.md` diff --git a/REBUILD-INSTRUCTIONS.md b/REBUILD-INSTRUCTIONS.md new file mode 100644 index 0000000..7a87d7b --- /dev/null +++ b/REBUILD-INSTRUCTIONS.md @@ -0,0 +1,98 @@ +# Инструкция по пересборке PROD и созданию бэкапов + +## 🎯 Что нужно сделать: + +### 1. Создать бэкап PROD БД + +```bash +# Сделать скрипты исполняемыми (первый раз) +chmod +x /var/www/service/backup/*.sh +chmod +x /var/www/service/platform/*.sh + +# Создать бэкап PROD БД +/var/www/service/backup/backup-all-db.sh +``` + +Это создаст бэкап: +- `/var/www/platform/prod/backups/platform_prod_db_YYYYMMDD_HHMMSS.sql.gz` + +**Примечание:** DEV БД не бэкапится, так как это окружение разработки. + +### 2. Пересобрать PROD окружение + +```bash +# Автоматическая пересборка (с бэкапом) +/var/www/service/platform/rebuild-prod.sh +``` + +Или вручную: + +```bash +cd /var/www/platform/prod + +# Остановить контейнеры +docker compose down + +# Пересобрать без кэша +docker compose build --no-cache --pull + +# Запустить +docker compose up -d + +# Подождать запуска БД +sleep 10 + +# Применить миграции +docker exec platform_prod_web python manage.py migrate + +# Проверить статус +docker compose ps +``` + +### 3. Проверить, что всё работает + +```bash +# Проверить логи +docker compose logs -f + +# Проверить подключение к БД +docker exec platform_prod_web python manage.py shell -c "from django.db import connection; print('DB:', connection.settings_dict['NAME'])" + +# Проверить количество пользователей (если таблица существует) +docker exec platform_prod_db psql -U platform_prod_user -d platform_prod_db -c "SELECT COUNT(*) FROM users_user;" 2>/dev/null || echo "Таблица не создана, нужно применить миграции" +``` + +### 4. Если пользователей нет - создать суперпользователя + +```bash +docker exec -it platform_prod_web python manage.py createsuperuser +``` + +## 📋 Что было исправлено: + +✅ **Отдельная сеть для prod** - `prod_network` (изолирована от dev) +✅ **Именованные volumes** - все volumes имеют префикс `platform_prod_` +✅ **Полные имена контейнеров** - используются `platform_prod_db` и `platform_prod_redis` +✅ **Защита от случайного удаления данных** - volumes не удаляются при `docker compose down` + +## ⚠️ Важно: + +- **НЕ используйте** `docker compose down --volumes` без бэкапа! +- Всегда создавайте бэкапы перед пересборкой +- Используйте `./safe-down.sh` для безопасной остановки + +## 🔄 Восстановление из бэкапа (если нужно): + +```bash +# Восстановить PROD БД +gunzip < /var/www/platform/prod/backups/platform_prod_db_*.sql.gz | docker exec -i platform_prod_db psql -U platform_prod_user -d postgres +``` + +## 📁 Расположение скриптов + +Все служебные скрипты находятся в `/var/www/service/`: + +- **Бэкапы**: `/var/www/service/backup/` +- **Управление платформой**: `/var/www/service/platform/` + +Подробнее: `/var/www/service/README.md` diff --git a/backend/apps/schedule/tasks.py b/backend/apps/schedule/tasks.py index 5b0d6cb..3986ba1 100644 --- a/backend/apps/schedule/tasks.py +++ b/backend/apps/schedule/tasks.py @@ -403,11 +403,12 @@ def start_lessons_automatically(): logger.info(f'Занятие {lesson.id} автоматически переведено в статус "in_progress"') # Находим занятия, которые уже прошли и должны быть завершены - # end_time < now (время окончания прошло) + # end_time < now - 5 минут (время окончания прошло более 5 минут назад - даём время на завершение) # status in ['scheduled', 'in_progress'] (еще не завершены) + five_minutes_ago = now - timedelta(minutes=5) lessons_to_complete = Lesson.objects.filter( status__in=['scheduled', 'in_progress'], - end_time__lt=now + end_time__lt=five_minutes_ago ).select_related('mentor', 'client') # Оптимизация: используем bulk_update вместо цикла с save() @@ -420,6 +421,22 @@ def start_lessons_automatically(): completed_count = len(lessons_to_complete_list) for lesson in lessons_to_complete_list: logger.info(f'Занятие {lesson.id} автоматически переведено в статус "completed" (время окончания прошло)') + + # Закрываем LiveKit комнату, если она есть + try: + from apps.video.models import VideoRoom + from apps.video.services import get_sfu_client, SFUClientError + + video_room = VideoRoom.objects.filter(lesson=lesson).first() + if video_room and video_room.room_id: + sfu_client = get_sfu_client() + try: + sfu_client.delete_room(str(video_room.room_id)) + logger.info(f'LiveKit комната {video_room.room_id} закрыта для урока {lesson.id}') + except SFUClientError as e: + logger.warning(f'Не удалось закрыть LiveKit комнату {video_room.room_id} для урока {lesson.id}: {e}') + except Exception as e: + logger.error(f'Ошибка закрытия LiveKit комнаты для урока {lesson.id}: {str(e)}', exc_info=True) if started_count > 0 or completed_count > 0: logger.info(f'[start_lessons_automatically] Начато: {started_count}, Завершено: {completed_count}') diff --git a/backup-all-db.sh b/backup-all-db.sh new file mode 100644 index 0000000..bd161fa --- /dev/null +++ b/backup-all-db.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Скрипт для создания бэкапов БД PROD и DEV + +set -e + +BACKUP_DIR="./backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) + +echo "==========================================" +echo "Создание бэкапов БД (PROD и DEV)" +echo "==========================================" +echo "" + +# Создать директорию для бэкапов +mkdir -p "$BACKUP_DIR" + +# Функция для создания бэкапа +backup_db() { + local CONTAINER_NAME=$1 + local DB_USER=$2 + local DB_NAME=$3 + local BACKUP_NAME=$4 + + echo "Создание бэкапа: $BACKUP_NAME" + + # Проверить, что контейнер запущен + if ! docker ps | grep -q "$CONTAINER_NAME"; then + echo "⚠️ Контейнер $CONTAINER_NAME не запущен, пропускаем..." + return 1 + fi + + BACKUP_FILE="$BACKUP_DIR/${BACKUP_NAME}_${TIMESTAMP}.sql.gz" + + # Создать бэкап + if docker exec "$CONTAINER_NAME" pg_dumpall -U "$DB_USER" -c 2>/dev/null | gzip > "$BACKUP_FILE"; then + BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) + echo " ✓ Бэкап создан: $BACKUP_FILE ($BACKUP_SIZE)" + return 0 + else + echo " ✗ Ошибка создания бэкапа для $BACKUP_NAME" + return 1 + fi +} + +# Бэкап PROD БД +echo "--- PROD БД ---" +backup_db "platform_prod_db" "platform_prod_user" "platform_prod_db" "platform_prod_db" + +echo "" + +# Бэкап DEV БД +echo "--- DEV БД ---" +backup_db "platform_dev_db" "platform_dev_user" "platform_dev_db" "platform_dev_db" + +echo "" +echo "==========================================" +echo "Бэкапы сохранены в: $BACKUP_DIR" +echo "==========================================" +echo "" +echo "Для восстановления PROD БД:" +echo " gunzip < $BACKUP_DIR/platform_prod_db_*.sql.gz | docker exec -i platform_prod_db psql -U platform_prod_user -d postgres" +echo "" +echo "Для восстановления DEV БД:" +echo " gunzip < $BACKUP_DIR/platform_dev_db_*.sql.gz | docker exec -i platform_dev_db psql -U platform_dev_user -d postgres" diff --git a/backup-db-auto.sh b/backup-db-auto.sh new file mode 100644 index 0000000..8f111d6 --- /dev/null +++ b/backup-db-auto.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# Автоматический скрипт для создания бэкапов БД PROD и DEV +# Запускается через cron дважды в день (00:00 и 12:00) + +set -e + +BACKUP_DIR="/var/www/platform/prod/backups" +LOG_FILE="/var/www/platform/prod/backups/backup.log" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +DATE=$(date +%Y-%m-%d\ %H:%M:%S) + +# Создать директорию для бэкапов и логов +mkdir -p "$BACKUP_DIR" + +# Функция для логирования +log() { + echo "[$DATE] $1" | tee -a "$LOG_FILE" +} + +log "==========================================" +log "Начало автоматического бэкапа БД" +log "==========================================" + +# Функция для создания бэкапа +backup_db() { + local CONTAINER_NAME=$1 + local DB_USER=$2 + local DB_NAME=$3 + local BACKUP_NAME=$4 + + log "Создание бэкапа: $BACKUP_NAME" + + # Проверить, что контейнер запущен + if ! docker ps | grep -q "$CONTAINER_NAME"; then + log "⚠️ Контейнер $CONTAINER_NAME не запущен, пропускаем..." + return 1 + fi + + BACKUP_FILE="$BACKUP_DIR/${BACKUP_NAME}_${TIMESTAMP}.sql.gz" + + # Создать бэкап + if docker exec "$CONTAINER_NAME" pg_dumpall -U "$DB_USER" -c 2>/dev/null | gzip > "$BACKUP_FILE"; then + BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) + log " ✓ Бэкап создан: $BACKUP_FILE ($BACKUP_SIZE)" + + # Проверить размер файла (должен быть больше 0) + if [ ! -s "$BACKUP_FILE" ]; then + log " ✗ ОШИБКА: Бэкап пустой!" + rm -f "$BACKUP_FILE" + return 1 + fi + + return 0 + else + log " ✗ Ошибка создания бэкапа для $BACKUP_NAME" + return 1 + fi +} + +# Бэкап PROD БД +PROD_SUCCESS=false +if backup_db "platform_prod_db" "platform_prod_user" "platform_prod_db" "platform_prod_db"; then + PROD_SUCCESS=true +fi + +# Бэкап DEV БД +DEV_SUCCESS=false +if backup_db "platform_dev_db" "platform_dev_user" "platform_dev_db" "platform_dev_db"; then + DEV_SUCCESS=true +fi + +# Очистка старых бэкапов (оставляем последние 30 дней) +log "Очистка старых бэкапов (старше 30 дней)..." +find "$BACKUP_DIR" -name "*.sql.gz" -type f -mtime +30 -delete 2>/dev/null || true +DELETED_COUNT=$(find "$BACKUP_DIR" -name "*.sql.gz" -type f 2>/dev/null | wc -l) +log "Осталось бэкапов: $DELETED_COUNT" + +# Итоги +log "==========================================" +if [ "$PROD_SUCCESS" = true ] && [ "$DEV_SUCCESS" = true ]; then + log "✓ Бэкапы созданы успешно (PROD и DEV)" +elif [ "$PROD_SUCCESS" = true ]; then + log "⚠️ Бэкап PROD создан, DEV пропущен" +elif [ "$DEV_SUCCESS" = true ]; then + log "⚠️ Бэкап DEV создан, PROD пропущен" +else + log "✗ Ошибка: бэкапы не созданы!" + exit 1 +fi +log "==========================================" + +# Проверка места на диске +DISK_USAGE=$(df -h "$BACKUP_DIR" | tail -1 | awk '{print $5}' | sed 's/%//') +if [ "$DISK_USAGE" -gt 80 ]; then + log "⚠️ ВНИМАНИЕ: Использовано дискового пространства: ${DISK_USAGE}%" +fi + +exit 0 diff --git a/backup-db.sh b/backup-db.sh new file mode 100644 index 0000000..ad9567c --- /dev/null +++ b/backup-db.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Скрипт для создания бэкапа БД PROD + +set -e + +BACKUP_DIR="./backups" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/platform_prod_db_backup_$TIMESTAMP.sql.gz" + +echo "==========================================" +echo "Создание бэкапа PROD БД" +echo "==========================================" +echo "" + +# Создать директорию для бэкапов +mkdir -p "$BACKUP_DIR" + +# Проверить, что контейнер БД запущен +if ! docker ps | grep -q platform_prod_db; then + echo "Ошибка: Контейнер platform_prod_db не запущен" + echo "Запустите БД: docker compose up -d db" + exit 1 +fi + +echo "Создание бэкапа..." +echo "Файл: $BACKUP_FILE" +echo "" + +# Создать бэкап +if docker exec platform_prod_db pg_dumpall -U platform_prod_user -c | gzip > "$BACKUP_FILE"; then + BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1) + echo "✓ Бэкап создан успешно" + echo " Размер: $BACKUP_SIZE" + echo " Файл: $BACKUP_FILE" + echo "" + echo "Для восстановления:" + echo " gunzip < $BACKUP_FILE | docker exec -i platform_prod_db psql -U platform_prod_user -d postgres" +else + echo "✗ Ошибка создания бэкапа!" + exit 1 +fi diff --git a/docker-compose.yml b/docker-compose.yml index 7e444a1..1ca80bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,11 @@ # front_material 3010, yjs 1236, excalidraw 3004, whiteboard 8083, # livekit 7880/7881, celery/beat — без портов (внутренние) # Dev использует: 5433, 6380, 8124, 8081, 3002, 1235, 3003, 8082, livekit 7890/7891 +# +# ВАЖНО: PROD использует отдельную сеть (prod_network) и именованные volumes +# НЕ используйте: docker compose down --volumes (удалит данные БД!) +# Используйте: docker compose down (остановит контейнеры, сохранит volumes) +# Для полной очистки: сначала сделайте бэкап БД, затем docker compose down --volumes services: db: @@ -20,7 +25,7 @@ services: volumes: - prod_postgres_data:/var/lib/postgresql/data networks: - - dev_network + - prod_network redis: image: redis:7-alpine @@ -31,7 +36,7 @@ services: volumes: - prod_redis_data:/data networks: - - dev_network + - prod_network web: build: @@ -44,13 +49,15 @@ services: # Daphne (ASGI): HTTP + WebSocket (/ws/notifications/, /ws/chat/, /ws/board/ и т.д.) command: sh -c "python manage.py migrate && python manage.py init_subjects && daphne -b 0.0.0.0 -p 8000 config.asgi:application" environment: - - DEBUG=${DEBUG:-True} - - SECRET_KEY=dev_secret_key - - ALLOWED_HOSTS=api.uchill.online,app.uchill.online,uchill.online,www.uchill.online,localhost,127.0.0.1,85.192.56.185 - - DATABASE_URL=postgresql://platform_prod_user:platform_prod_password@db:5432/platform_prod_db - - REDIS_URL=redis://redis:6379/0 - - CELERY_BROKER_URL=redis://redis:6379/1 - - CELERY_RESULT_BACKEND=redis://redis:6379/2 + - DEBUG=${DEBUG:-False} + - SECRET_KEY=${SECRET_KEY} + - ALLOWED_HOSTS=${ALLOWED_HOSTS} + - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS} + - CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS} + - DATABASE_URL=postgresql://platform_prod_user:platform_prod_password@platform_prod_db:5432/platform_prod_db + - REDIS_URL=redis://platform_prod_redis:6379/0 + - CELERY_BROKER_URL=redis://platform_prod_redis:6379/1 + - CELERY_RESULT_BACKEND=redis://platform_prod_redis:6379/2 # Явно передаём переменные почты из .env (иначе контейнер может не видеть их) - EMAIL_BACKEND=${EMAIL_BACKEND:-smtp} - EMAIL_HOST=${EMAIL_HOST} @@ -78,7 +85,7 @@ services: - db - redis networks: - - dev_network + - prod_network celery: build: @@ -90,11 +97,11 @@ services: env_file: .env command: celery -A config worker -l info environment: - - DEBUG=${DEBUG:-True} - - DATABASE_URL=postgresql://platform_prod_user:platform_prod_password@db:5432/platform_prod_db - - REDIS_URL=redis://redis:6379/0 - - CELERY_BROKER_URL=redis://redis:6379/1 - - CELERY_RESULT_BACKEND=redis://redis:6379/2 + - DEBUG=${DEBUG:-False} + - DATABASE_URL=postgresql://platform_prod_user:platform_prod_password@platform_prod_db:5432/platform_prod_db + - REDIS_URL=redis://platform_prod_redis:6379/0 + - CELERY_BROKER_URL=redis://platform_prod_redis:6379/1 + - CELERY_RESULT_BACKEND=redis://platform_prod_redis:6379/2 - EMAIL_BACKEND=${EMAIL_BACKEND:-smtp} - EMAIL_HOST=${EMAIL_HOST} - EMAIL_PORT=${EMAIL_PORT:-2525} @@ -116,7 +123,7 @@ services: - redis - web networks: - - dev_network + - prod_network celery-beat: build: @@ -128,11 +135,11 @@ services: env_file: .env command: celery -A config beat -l info environment: - - DEBUG=${DEBUG:-True} - - DATABASE_URL=postgresql://platform_prod_user:platform_prod_password@db:5432/platform_prod_db - - REDIS_URL=redis://redis:6379/0 - - CELERY_BROKER_URL=redis://redis:6379/1 - - CELERY_RESULT_BACKEND=redis://redis:6379/2 + - DEBUG=${DEBUG:-False} + - DATABASE_URL=postgresql://platform_prod_user:platform_prod_password@platform_prod_db:5432/platform_prod_db + - REDIS_URL=redis://platform_prod_redis:6379/0 + - CELERY_BROKER_URL=redis://platform_prod_redis:6379/1 + - CELERY_RESULT_BACKEND=redis://platform_prod_redis:6379/2 - EMAIL_BACKEND=${EMAIL_BACKEND:-smtp} - EMAIL_HOST=${EMAIL_HOST} - EMAIL_PORT=${EMAIL_PORT:-2525} @@ -154,7 +161,7 @@ services: - redis - web networks: - - dev_network + - prod_network # Telegram бот (polling): получает /start, /link <код> и т.д. Если используете webhook — не поднимайте этот сервис. telegram-bot: @@ -167,9 +174,9 @@ services: env_file: .env command: python manage.py runtelegrambot environment: - - DEBUG=${DEBUG:-True} - - DATABASE_URL=postgresql://platform_prod_user:platform_prod_password@db:5432/platform_prod_db - - REDIS_URL=redis://redis:6379/0 + - DEBUG=${DEBUG:-False} + - DATABASE_URL=postgresql://platform_prod_user:platform_prod_password@platform_prod_db:5432/platform_prod_db + - REDIS_URL=redis://platform_prod_redis:6379/0 - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} - TELEGRAM_USE_WEBHOOK=${TELEGRAM_USE_WEBHOOK:-False} - TELEGRAM_WEBHOOK_URL=${TELEGRAM_WEBHOOK_URL:-} @@ -181,7 +188,7 @@ services: - redis - web networks: - - dev_network + - prod_network # Видеоуроки: хост nginx (api.uchill.online) проксирует /livekit на 7880. Dev на том же хосте — 7890. # LIVEKIT_KEYS — строго один ключ в формате "key: secret" (пробел после двоеточия). В .env задайте одну строку: LIVEKIT_KEYS=APIKeyPlatform2024Secret: ThisIsAVerySecureSecretKeyForPlatform2024VideoConf @@ -196,7 +203,7 @@ services: - "7880:7880" - "7881:7881" networks: - - dev_network + - prod_network nginx: image: nginx:alpine @@ -210,32 +217,31 @@ services: depends_on: - web networks: - - dev_network + - prod_network front_material: build: context: ./front_material dockerfile: Dockerfile - target: development + target: production + args: + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + - NEXT_PUBLIC_WS_URL=${NEXT_PUBLIC_WS_URL} + - NEXT_PUBLIC_LIVEKIT_URL=${NEXT_PUBLIC_LIVEKIT_URL} + - NEXT_PUBLIC_EXCALIDRAW_URL=${NEXT_PUBLIC_EXCALIDRAW_URL:-} container_name: platform_prod_front_material restart: unless-stopped env_file: .env environment: - - NODE_ENV=development - - WATCHPACK_POLLING=true + - NODE_ENV=production - HOSTNAME=0.0.0.0 - - CHOKIDAR_USEPOLLING=true # Доска: поддомен board.uchill.online (прокси nginx на 3004) или путь на том же домене - NEXT_PUBLIC_EXCALIDRAW_URL=${NEXT_PUBLIC_EXCALIDRAW_URL:-} - NEXT_PUBLIC_EXCALIDRAW_PATH=${NEXT_PUBLIC_EXCALIDRAW_PATH:-/excalidraw} ports: - "3010:3000" - volumes: - - ./front_material:/app - - front_material_node_modules:/app/node_modules - - front_material_next:/app/.next networks: - - dev_network + - prod_network yjs-whiteboard: build: @@ -246,21 +252,23 @@ services: ports: - "1236:1234" networks: - - dev_network + - prod_network excalidraw: build: context: ./excalidraw-server dockerfile: Dockerfile + target: production container_name: platform_prod_excalidraw restart: unless-stopped environment: - # basePath в next.config.js: иначе /_next/ запросы уходят на основной фронт и доска пустая - - NEXT_PUBLIC_BASE_PATH=/excalidraw + - NODE_ENV=production + # Поддомен board.uchill.online: basePath не нужен (приложение на корне поддомена) + - NEXT_PUBLIC_BASE_PATH= ports: - "3004:3001" networks: - - dev_network + - prod_network whiteboard: build: @@ -271,14 +279,21 @@ services: ports: - "8083:8080" networks: - - dev_network + - prod_network volumes: + # ВАЖНО: Эти volumes содержат данные БД и Redis + # НЕ используйте docker compose down --volumes без бэкапа! prod_postgres_data: + name: platform_prod_postgres_data prod_redis_data: + name: platform_prod_redis_data front_material_node_modules: + name: platform_prod_front_material_node_modules front_material_next: + name: platform_prod_front_material_next networks: - dev_network: + prod_network: + name: platform_prod_network driver: bridge diff --git a/excalidraw-server/Dockerfile b/excalidraw-server/Dockerfile index 91b2edb..6bf0de9 100644 --- a/excalidraw-server/Dockerfile +++ b/excalidraw-server/Dockerfile @@ -1,20 +1,49 @@ -FROM node:18-alpine - -WORKDIR /app - -# Копируем package.json и patches (для patch-package) -COPY package*.json ./ -COPY patches ./patches/ - -# Устанавливаем зависимости (postinstall применит патч y-excalidraw) -RUN npm install - -# Гарантированно применяем патчи (fix generateKeyBetween при вставке изображения) -RUN npx patch-package - -# Копируем все файлы -COPY . . - -# Запуск в dev режиме -CMD ["npm", "run", "dev"] - +# Multi-stage build для production +FROM node:18-alpine AS builder + +WORKDIR /app + +# Копируем package.json и patches +COPY package*.json ./ +COPY patches ./patches/ + +# Устанавливаем зависимости +RUN npm ci + +# Гарантированно применяем патчи +RUN npx patch-package + +# Копируем исходный код +COPY . . + +# Собираем приложение +ENV NODE_ENV=production +RUN npm run build + +# Production stage +FROM node:18-alpine AS production + +WORKDIR /app + +ENV NODE_ENV=production + +# Копируем собранное приложение (standalone mode) +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +# Создаём пустую директорию public (если её нет в проекте, Next.js может её использовать) +RUN mkdir -p ./public + +# Создаем непривилегированного пользователя +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs && \ + chown -R nextjs:nodejs /app + +USER nextjs + +EXPOSE 3001 + +ENV PORT=3001 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] + diff --git a/excalidraw-server/src/app/page.tsx b/excalidraw-server/src/app/page.tsx index cf5e30d..e4e32d4 100644 --- a/excalidraw-server/src/app/page.tsx +++ b/excalidraw-server/src/app/page.tsx @@ -89,11 +89,16 @@ export default function ExcalidrawPage() { setBoardId(id); - // Yjs: хост из apiUrl - const port = params.get('yjsPort') || '1234'; + // Yjs: через nginx /yjs (prod) или прямой порт (dev/localhost) const apiHost = new URL(api).hostname; + const isLocalhost = apiHost === 'localhost' || apiHost === '127.0.0.1'; const wsProtocol = api.startsWith('https') ? 'wss:' : 'ws:'; - setWsUrl(`${wsProtocol}//${apiHost}:${port}`); + const yjsPort = params.get('yjsPort') || '1234'; + // Prod: через nginx /yjs (без порта), Dev: прямой порт + const wsUrl = isLocalhost + ? `${wsProtocol}//${apiHost}:${yjsPort}` + : `${wsProtocol}//${apiHost}/yjs`; + setWsUrl(wsUrl); // Имя из API (UTF-8, без проблем с кодировкой URL/postMessage) if (token) { @@ -229,7 +234,6 @@ export default function ExcalidrawPage() { }, [username]); if (!boardId) { - const example = `${window.location.origin}${window.location.pathname}?boardId=your-board-id&apiUrl=http://127.0.0.1:8123`; return (
?boardId=xxx&apiUrl=http://127.0.0.1:8123 -

Пример:

- - {example} -
); } diff --git a/front_material/Dockerfile b/front_material/Dockerfile index 1687423..997872f 100644 --- a/front_material/Dockerfile +++ b/front_material/Dockerfile @@ -9,10 +9,12 @@ WORKDIR /app ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_WS_URL ARG NEXT_PUBLIC_LIVEKIT_URL +ARG NEXT_PUBLIC_EXCALIDRAW_URL ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL ENV NEXT_PUBLIC_WS_URL=$NEXT_PUBLIC_WS_URL ENV NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL +ENV NEXT_PUBLIC_EXCALIDRAW_URL=$NEXT_PUBLIC_EXCALIDRAW_URL ENV NODE_ENV=development ENV HOSTNAME=0.0.0.0 ENV WATCHPACK_POLLING=true @@ -59,10 +61,12 @@ WORKDIR /app ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_WS_URL ARG NEXT_PUBLIC_LIVEKIT_URL +ARG NEXT_PUBLIC_EXCALIDRAW_URL ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL ENV NEXT_PUBLIC_WS_URL=$NEXT_PUBLIC_WS_URL ENV NEXT_PUBLIC_LIVEKIT_URL=$NEXT_PUBLIC_LIVEKIT_URL +ENV NEXT_PUBLIC_EXCALIDRAW_URL=$NEXT_PUBLIC_EXCALIDRAW_URL # Копируем package files COPY package*.json ./ diff --git a/front_material/components/auth/AuthRedirect.tsx b/front_material/components/auth/AuthRedirect.tsx index 04d5d35..0b09ada 100644 --- a/front_material/components/auth/AuthRedirect.tsx +++ b/front_material/components/auth/AuthRedirect.tsx @@ -1,45 +1,45 @@ -'use client'; - -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { useAuth } from '@/contexts/AuthContext'; - -/** - * Если пользователь авторизован — редирект на дашборд. - * Страницы логина/регистрации и т.д. не должны быть доступны авторизованным. - */ -export function AuthRedirect({ children }: { children: React.ReactNode }) { - const router = useRouter(); - const { user, loading } = useAuth(); - - useEffect(() => { - if (loading) return; - if (user) { - router.replace('/dashboard'); - } - }, [user, loading, router]); - - if (loading) { - return ( -
-
- Загрузка... -
-
- ); - } - - if (user) { - return null; // редирект уже идёт - } - - return <>{children}; -} +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuth } from '@/contexts/AuthContext'; + +/** + * Если пользователь авторизован — редирект на дашборд. + * Страницы логина/регистрации и т.д. не должны быть доступны авторизованным. + */ +export function AuthRedirect({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const { user, loading } = useAuth(); + + useEffect(() => { + if (loading) return; + if (user) { + router.replace('/dashboard'); + } + }, [user, loading, router]); + + if (loading) { + return ( +
+
+ Загрузка... +
+
+ ); + } + + if (user) { + return null; // редирект уже идёт + } + + return <>{children}; +} diff --git a/front_material/components/board/WhiteboardIframe.tsx b/front_material/components/board/WhiteboardIframe.tsx index ae24042..e82735a 100644 --- a/front_material/components/board/WhiteboardIframe.tsx +++ b/front_material/components/board/WhiteboardIframe.tsx @@ -51,6 +51,9 @@ export function WhiteboardIframe({ })(); url.searchParams.set('boardId', boardId); url.searchParams.set('apiUrl', apiUrl); + // Yjs WebSocket порт: 1236 (внешний порт yjs-whiteboard контейнера) или через nginx прокси + const yjsPort = process.env.NEXT_PUBLIC_YJS_PORT || '1236'; + url.searchParams.set('yjsPort', yjsPort); if (token) url.searchParams.set('token', token); if (isMentor) url.searchParams.set('isMentor', '1'); diff --git a/rebuild-prod.sh b/rebuild-prod.sh new file mode 100644 index 0000000..9b79d98 --- /dev/null +++ b/rebuild-prod.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Скрипт для полной пересборки PROD окружения с бэкапом БД + +set -e + +echo "==========================================" +echo "Полная пересборка PROD окружения" +echo "==========================================" +echo "" +echo "⚠️ ВНИМАНИЕ: Это пересоберёт все контейнеры без кэша" +echo "" + +cd "$(dirname "$0")" + +# Шаг 1: Создать бэкап БД +echo "Шаг 1: Создание бэкапа БД..." +if [ -f "./backup-all-db.sh" ]; then + ./backup-all-db.sh +else + echo "⚠️ Скрипт backup-all-db.sh не найден, создаём бэкап вручную..." + mkdir -p ./backups + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + if docker ps | grep -q platform_prod_db; then + docker exec platform_prod_db pg_dumpall -U platform_prod_user -c | gzip > "./backups/platform_prod_db_backup_${TIMESTAMP}.sql.gz" + echo "✓ Бэкап создан: ./backups/platform_prod_db_backup_${TIMESTAMP}.sql.gz" + else + echo "⚠️ Контейнер БД не запущен, пропускаем бэкап" + fi +fi + +echo "" +echo "Шаг 2: Остановка контейнеров..." +docker compose down + +echo "" +echo "Шаг 3: Пересборка образов без кэша..." +echo "Это может занять несколько минут..." +docker compose build --no-cache --pull + +echo "" +echo "Шаг 4: Запуск контейнеров..." +docker compose up -d + +echo "" +echo "Шаг 5: Ожидание запуска БД..." +sleep 10 + +echo "" +echo "Шаг 6: Применение миграций..." +docker exec platform_prod_web python manage.py migrate + +echo "" +echo "Шаг 7: Проверка статуса контейнеров..." +docker compose ps + +echo "" +echo "==========================================" +echo "✓ Пересборка завершена!" +echo "==========================================" +echo "" +echo "Проверьте логи:" +echo " docker compose logs -f" +echo "" +echo "Если нужно создать суперпользователя:" +echo " docker exec -it platform_prod_web python manage.py createsuperuser" diff --git a/remove-cron-backup.sh b/remove-cron-backup.sh new file mode 100644 index 0000000..943b4fd --- /dev/null +++ b/remove-cron-backup.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Скрипт для удаления автоматического бэкапа из cron + +set -e + +SCRIPT_DIR="/var/www/platform/prod" +BACKUP_SCRIPT="$SCRIPT_DIR/backup-db-auto.sh" +CRON_USER="root" + +echo "==========================================" +echo "Удаление автоматического бэкапа из cron" +echo "==========================================" +echo "" + +# Проверить, есть ли запись в crontab +if crontab -u "$CRON_USER" -l 2>/dev/null | grep -q "$BACKUP_SCRIPT"; then + echo "Найдена запись в crontab:" + crontab -u "$CRON_USER" -l | grep "$BACKUP_SCRIPT" + echo "" + read -p "Удалить? (y/N): " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Yy]$ ]]; then + crontab -u "$CRON_USER" -l 2>/dev/null | grep -v "$BACKUP_SCRIPT" | crontab -u "$CRON_USER" - + echo "✓ Запись удалена из crontab" + else + echo "Отменено." + fi +else + echo "Запись в crontab не найдена." +fi diff --git a/safe-down.sh b/safe-down.sh new file mode 100644 index 0000000..313036f --- /dev/null +++ b/safe-down.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Безопасная остановка PROD окружения +# Этот скрипт останавливает контейнеры БЕЗ удаления volumes (данных БД) + +set -e + +echo "==========================================" +echo "Безопасная остановка PROD окружения" +echo "==========================================" +echo "" +echo "Это остановит контейнеры, но СОХРАНИТ данные БД и Redis" +echo "" + +cd "$(dirname "$0")" + +# Остановить контейнеры без удаления volumes +docker compose down + +echo "" +echo "✓ Контейнеры остановлены" +echo "✓ Volumes сохранены (данные БД не потеряны)" +echo "" +echo "Для запуска: docker compose up -d" +echo "Для полной очистки (с удалением данных): docker compose down --volumes" +echo " (ВНИМАНИЕ: это удалит все данные БД!)" diff --git a/setup-cron-backup.sh b/setup-cron-backup.sh new file mode 100644 index 0000000..52953ae --- /dev/null +++ b/setup-cron-backup.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Скрипт для настройки автоматического бэкапа БД через cron +# Запускается дважды в день: в 00:00 и 12:00 + +set -e + +SCRIPT_DIR="/var/www/platform/prod" +BACKUP_SCRIPT="$SCRIPT_DIR/backup-db-auto.sh" +CRON_USER="root" + +echo "==========================================" +echo "Настройка автоматического бэкапа БД" +echo "==========================================" +echo "" + +# Проверить, что скрипт существует +if [ ! -f "$BACKUP_SCRIPT" ]; then + echo "Ошибка: Скрипт $BACKUP_SCRIPT не найден!" + exit 1 +fi + +# Сделать скрипт исполняемым +chmod +x "$BACKUP_SCRIPT" +echo "✓ Скрипт сделан исполняемым" + +# Создать директорию для бэкапов +mkdir -p "$SCRIPT_DIR/backups" +echo "✓ Директория для бэкапов создана" + +# Найти путь к docker (для cron) +DOCKER_PATH=$(which docker 2>/dev/null || echo "/usr/bin/docker") + +# Проверить, есть ли уже запись в crontab +# Используем PATH с docker и bash для надежности +CRON_CMD="0 0,12 * * * PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin $BACKUP_SCRIPT >> $SCRIPT_DIR/backups/cron.log 2>&1" + +if crontab -u "$CRON_USER" -l 2>/dev/null | grep -q "$BACKUP_SCRIPT"; then + echo "⚠️ Запись в crontab уже существует" + echo "" + echo "Текущий crontab:" + crontab -u "$CRON_USER" -l | grep "$BACKUP_SCRIPT" + echo "" + read -p "Заменить существующую запись? (y/N): " -n 1 -r + echo "" + if [[ $REPLY =~ ^[Yy]$ ]]; then + # Удалить старую запись + crontab -u "$CRON_USER" -l 2>/dev/null | grep -v "$BACKUP_SCRIPT" | crontab -u "$CRON_USER" - + # Добавить новую + (crontab -u "$CRON_USER" -l 2>/dev/null; echo "$CRON_CMD") | crontab -u "$CRON_USER" - + echo "✓ Запись в crontab обновлена" + else + echo "Отменено. Существующая запись сохранена." + exit 0 + fi +else + # Добавить новую запись + (crontab -u "$CRON_USER" -l 2>/dev/null; echo "$CRON_CMD") | crontab -u "$CRON_USER" - + echo "✓ Запись в crontab добавлена" +fi + +echo "" +echo "==========================================" +echo "Настройка завершена!" +echo "==========================================" +echo "" +echo "Расписание бэкапов:" +echo " - Каждый день в 00:00 (полночь)" +echo " - Каждый день в 12:00 (полдень)" +echo "" +echo "Проверить crontab:" +echo " crontab -u $CRON_USER -l" +echo "" +echo "Просмотр логов бэкапов:" +echo " tail -f $SCRIPT_DIR/backups/backup.log" +echo " tail -f $SCRIPT_DIR/backups/cron.log" +echo "" +echo "Удалить автоматический бэкап:" +echo " crontab -u $CRON_USER -l | grep -v '$BACKUP_SCRIPT' | crontab -u $CRON_USER -"