full
Deploy to Production / deploy-production (push) Successful in 26s Details

This commit is contained in:
root 2026-02-13 19:26:30 +03:00
parent 083fd4d826
commit 118f33f77b
17 changed files with 933 additions and 118 deletions

93
AUTO-BACKUP-SETUP.md Normal file
View File

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

148
README-PROD.md Normal file
View File

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

98
REBUILD-INSTRUCTIONS.md Normal file
View File

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

View File

@ -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()
@ -421,6 +422,22 @@ def start_lessons_automatically():
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}')

65
backup-all-db.sh Normal file
View File

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

99
backup-db-auto.sh Normal file
View File

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

42
backup-db.sh Normal file
View File

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

View File

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

View File

@ -1,20 +1,49 @@
FROM node:18-alpine
# Multi-stage build для production
FROM node:18-alpine AS builder
WORKDIR /app
# Копируем package.json и patches (для patch-package)
# Копируем package.json и patches
COPY package*.json ./
COPY patches ./patches/
# Устанавливаем зависимости (postinstall применит патч y-excalidraw)
RUN npm install
# Устанавливаем зависимости
RUN npm ci
# Гарантированно применяем патчи (fix generateKeyBetween при вставке изображения)
# Гарантированно применяем патчи
RUN npx patch-package
# Копируем все файлы
# Копируем исходный код
COPY . .
# Запуск в dev режиме
CMD ["npm", "run", "dev"]
# Собираем приложение
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"]

View File

@ -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 (
<div style={{
display: 'flex',
@ -247,10 +251,6 @@ export default function ExcalidrawPage() {
<code style={{ fontSize: 12, background: '#f5f5f5', padding: '8px 12px', borderRadius: 8, wordBreak: 'break-all' }}>
?boardId=xxx&apiUrl=http://127.0.0.1:8123
</code>
<p style={{ margin: 0, fontSize: 14, color: '#666' }}>Пример:</p>
<code style={{ fontSize: 11, background: '#f5f5f5', padding: '8px 12px', borderRadius: 8, wordBreak: 'break-all', maxWidth: '100%' }}>
{example}
</code>
</div>
);
}

View File

@ -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 ./

View File

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

66
rebuild-prod.sh Normal file
View File

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

31
remove-cron-backup.sh Normal file
View File

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

26
safe-down.sh Normal file
View File

@ -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 " (ВНИМАНИЕ: это удалит все данные БД!)"

79
setup-cron-backup.sh Normal file
View File

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