Add staging environment plan and deploy script

Staging runs at staging.school.second-brain.ru on Hetzner port 3011.
Deploy script: ~/Documents/Claude/scripts/deploy-staging.sh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 15:13:26 +05:00
parent 30405e768b
commit 38267fa432
@@ -0,0 +1,451 @@
# LMS Staging Environment Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Развернуть полноценный staging-стенд LMS на `staging.school.second-brain.ru` — изолированный от prod, с автоматическим применением миграций и отключёнными email-уведомлениями.
**Architecture:** Hetzner VPS (178.104.27.196, 16 GB RAM), отдельный Docker Compose стек в `/root/digital-household/lms-staging/` с собственным postgres-контейнером. Caddy на том же сервере роутит `staging.school.second-brain.ru → localhost:3011`. Сборочный репо — `/root/lms-staging-build/`, тег образа — `lms-staging-app:latest`. Деплой в staging производится отдельным скриптом `deploy-staging.sh` независимо от prod.
**Tech Stack:** Docker Compose, Next.js 16 (App Router), PostgreSQL 16, Caddy, Gitea (git.second-brain.ru/admins/lms-sb)
---
## Файловая структура
```
Hetzner VPS (root@178.104.27.196):
├── /root/lms-staging-build/ ← git clone (сборочная директория)
│ └── [полный репо lms-sb]
├── /root/digital-household/lms-staging/
│ ├── docker-compose.yml ← создаём (app + db)
│ └── .env ← создаём (staging-специфичный)
└── /root/digital-household/caddy-proxy/
└── Caddyfile ← добавляем блок staging
Mac:
└── ~/Documents/Claude/scripts/deploy-staging.sh ← создаём
```
---
## Task 1: Клонировать репо на Hetzner
**Files:**
- Создаём: `/root/lms-staging-build/` на сервере
- [ ] **Шаг 1.1: SSH на Hetzner и клонировать репо**
Нужен токен Gitea. Он хранится в уже существующем git remote на сервере. Достанем его:
```bash
ssh root@178.104.27.196 "git -C /root/digital-household/lms-sb remote get-url origin 2>/dev/null || echo 'no-remote'"
```
Если remote есть — берём URL из него. Если нет — токен в `~/.config/secrets/` на Mac (найти файл с `GITEA_TOKEN` или `git.second-brain.ru`).
- [ ] **Шаг 1.2: Клонировать**
```bash
GITEA_URL=$(ssh root@178.104.27.196 "git -C /root/digital-household/lms-sb remote get-url origin 2>/dev/null")
# Пример: https://admins:TOKEN@git.second-brain.ru/admins/lms-sb.git
ssh root@178.104.27.196 "git clone ${GITEA_URL} /root/lms-staging-build"
```
- [ ] **Шаг 1.3: Проверить**
```bash
ssh root@178.104.27.196 "ls /root/lms-staging-build/src && git -C /root/lms-staging-build log --oneline -3"
```
Ожидаем: список файлов и последние 3 коммита.
---
## Task 2: Создать staging docker-compose.yml
**Files:**
- Создаём: `/root/digital-household/lms-staging/docker-compose.yml`
- [ ] **Шаг 2.1: Создать директорию**
```bash
ssh root@178.104.27.196 "mkdir -p /root/digital-household/lms-staging"
```
- [ ] **Шаг 2.2: Записать docker-compose.yml**
```bash
ssh root@178.104.27.196 "cat > /root/digital-household/lms-staging/docker-compose.yml" << 'COMPOSE'
services:
app:
image: lms-staging-app:latest
restart: unless-stopped
ports:
- "3011:3000"
env_file: .env
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: lms_staging_user
POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: lms_staging_db
volumes:
- staging_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lms_staging_user -d lms_staging_db"]
interval: 5s
timeout: 5s
retries: 10
volumes:
staging_postgres_data:
COMPOSE
```
- [ ] **Шаг 2.3: Проверить**
```bash
ssh root@178.104.27.196 "cat /root/digital-household/lms-staging/docker-compose.yml"
```
Ожидаем: полный YAML без артефактов heredoc.
---
## Task 3: Залить staging .env на сервер
**Files:**
- Создаём: `/root/digital-household/lms-staging/.env`
Все креды уже сгенерированы и хранятся в `~/.config/secrets/lms-staging.env` на Mac.
- [ ] **Шаг 3.1: Скопировать .env на сервер**
```bash
scp ~/.config/secrets/lms-staging.env root@178.104.27.196:/root/digital-household/lms-staging/.env
```
- [ ] **Шаг 3.2: Установить права**
```bash
ssh root@178.104.27.196 "chmod 600 /root/digital-household/lms-staging/.env"
```
- [ ] **Шаг 3.3: Проверить**
```bash
ssh root@178.104.27.196 "grep -E '^BETTER_AUTH_URL|^RESEND_API_KEY|^DB_PASSWORD' /root/digital-household/lms-staging/.env"
```
Ожидаем:
```
BETTER_AUTH_URL=https://staging.school.second-brain.ru
RESEND_API_KEY=
DB_PASSWORD=38002343e67efd057559d8f073d45892
```
---
## Task 4: Собрать Docker-образ и запустить staging
**Files:**
- Используем `/root/lms-staging-build/` для сборки
- [ ] **Шаг 4.1: Собрать образ**
```bash
ssh root@178.104.27.196 "cd /root/lms-staging-build && docker build -t lms-staging-app:latest . 2>&1 | tail -5"
```
Ожидаем: `Successfully built ...` или `naming to docker.io/library/lms-staging-app:latest done`.
Время: ~2–4 минуты.
- [ ] **Шаг 4.2: Запустить контейнеры**
```bash
ssh root@178.104.27.196 "cd /root/digital-household/lms-staging && docker compose up -d"
```
Ожидаем: `Container lms-staging-db-1 Started`, `Container lms-staging-app-1 Started`.
- [ ] **Шаг 4.3: Проверить логи (через 10 сек)**
```bash
ssh root@178.104.27.196 "sleep 10 && docker logs lms-staging-app-1 --tail 20"
```
Ожидаем в логах:
- `Running database migrations...`
- `No pending migrations to apply.` (или список применённых)
- `Starting Next.js...`
- `✓ Ready in 0ms`
Если вместо этого ошибка `DATABASE_URL` или `connection refused` — проверить `.env`: `docker exec lms-staging-app-1 env | grep DATABASE`.
---
## Task 5: Настроить Caddy
**Files:**
- Изменяем: `/root/digital-household/caddy-proxy/Caddyfile`
- [ ] **Шаг 5.1: Посмотреть текущий конец Caddyfile**
```bash
ssh root@178.104.27.196 "tail -10 /root/digital-household/caddy-proxy/Caddyfile"
```
- [ ] **Шаг 5.2: Добавить блок для staging**
```bash
ssh root@178.104.27.196 "cat >> /root/digital-household/caddy-proxy/Caddyfile" << 'CADDY'
staging.school.second-brain.ru {
reverse_proxy http://localhost:3011
}
CADDY
```
- [ ] **Шаг 5.3: Перезагрузить Caddy**
```bash
ssh root@178.104.27.196 "docker exec caddy-proxy caddy reload --config /etc/caddy/Caddyfile 2>&1"
```
Ожидаем: пустой вывод (успех) или `Successfully loaded new configuration`.
Если ошибка синтаксиса — проверить: `docker exec caddy-proxy caddy validate --config /etc/caddy/Caddyfile`.
---
## Task 6: Добавить DNS-запись
DNS для `*.school.second-brain.ru` и `school.second-brain.ru` нужно проверить — где управляется зона.
- [ ] **Шаг 6.1: Проверить текущий DNS**
```bash
dig +short staging.school.second-brain.ru
dig +short school.second-brain.ru
```
Если `staging.school.second-brain.ru` не резолвится — нужно добавить A-запись.
- [ ] **Шаг 6.2: Добавить A-запись**
В панели DNS (Hetzner DNS Console или CloudFlare — где управляется зона `second-brain.ru`):
```
Тип: A
Имя: staging.school
Значение: 178.104.27.196
TTL: 300
```
- [ ] **Шаг 6.3: Дождаться распространения**
```bash
# Через 1-2 минуты:
dig +short staging.school.second-brain.ru
```
Ожидаем: `178.104.27.196`
---
## Task 7: Проверить работу staging end-to-end
- [ ] **Шаг 7.1: HTTP проверка**
```bash
curl -s -o /dev/null -w "%{http_code}" https://staging.school.second-brain.ru/login
```
Ожидаем: `200`
- [ ] **Шаг 7.2: Войти под тестовым аккаунтом**
Открыть `https://staging.school.second-brain.ru/login` в браузере.
БД пустая, но seed создаёт admin-аккаунт. Запустить seed:
```bash
ssh root@178.104.27.196 "cd /root/lms-staging-build && docker compose -f /root/digital-household/lms-staging/docker-compose.yml exec app npx prisma db seed 2>&1 | tail -5"
```
После seed входить: `admin@second-brain.ru` / `Password123!`
- [ ] **Шаг 7.3: Проверить что email не уходит**
В staging при регистрации `/api/auth/sign-up` не должно быть ошибок Resend (только `RESEND_API_KEY` пустой — письмо тихо не отправляется, регистрация всё равно работает).
Убедиться в логах:
```bash
ssh root@178.104.27.196 "docker logs lms-staging-app-1 --since 5m 2>&1 | grep -i 'resend\|email\|error' | head -10"
```
---
## Task 8: Скрипт deploy-to-staging и документация
**Files:**
- Создаём: `~/Documents/Claude/scripts/deploy-staging.sh`
- Создаём: `SBT/02-Стандарты/Процессы/lms-staging-deploy.md`
- [ ] **Шаг 8.1: Написать скрипт deploy-staging.sh**
```bash
cat > ~/Documents/Claude/scripts/deploy-staging.sh << 'SCRIPT'
#!/usr/bin/env bash
# Deploy LMS to staging (staging.school.second-brain.ru)
# Usage: bash deploy-staging.sh
set -euo pipefail
STAGING_SERVER="root@178.104.27.196"
BUILD_DIR="/root/lms-staging-build"
COMPOSE_DIR="/root/digital-household/lms-staging"
LOG_LINES=15
echo "=== LMS Staging Deploy ==="
echo "→ Pulling latest code..."
ssh "$STAGING_SERVER" "cd ${BUILD_DIR} && git pull 2>&1 | tail -5"
echo "→ Building image..."
ssh "$STAGING_SERVER" "cd ${BUILD_DIR} && docker build -t lms-staging-app:latest . 2>&1 | tail -5"
echo "→ Restarting app..."
ssh "$STAGING_SERVER" "cd ${COMPOSE_DIR} && docker compose up -d app"
echo "→ Waiting 10s for startup..."
sleep 10
echo "→ Logs:"
ssh "$STAGING_SERVER" "docker logs lms-staging-app-1 --tail ${LOG_LINES} 2>&1"
echo "→ Health check..."
CODE=$(curl -s -o /dev/null -w "%{http_code}" https://staging.school.second-brain.ru/login)
if [ "$CODE" = "200" ]; then
echo "✅ Staging OK — https://staging.school.second-brain.ru (HTTP $CODE)"
else
echo "❌ Staging returned HTTP $CODE"
exit 1
fi
SCRIPT
chmod +x ~/Documents/Claude/scripts/deploy-staging.sh
```
- [ ] **Шаг 8.2: Тестовый запуск скрипта**
```bash
bash ~/Documents/Claude/scripts/deploy-staging.sh
```
Ожидаем в конце: `✅ Staging OK — https://staging.school.second-brain.ru (HTTP 200)`
- [ ] **Шаг 8.3: Создать SBT-заметку**
Создать файл `SBT/02-Стандарты/Процессы/lms-staging-deploy.md` со следующим содержимым:
```markdown
---
type: process
title: "LMS — Деплой в staging"
status: active
created: 20260525
updated: 20260525
tags: [lms, staging, deploy, docker]
related: ["[[Процессы/lms-deploy]]", "[[Сервисы/hoster-kz]]", "[[Сервисы/vps-hetzner]]"]
---
# Процесс: Деплой LMS в staging
## Когда применяется
Перед выкаткой любых изменений в prod — сначала деплоим в staging и проверяем.
Также: тестирование миграций БД, проверка новых фич, отладка.
## Ключевые факты
| Что | Где |
|---|---|
| URL | https://staging.school.second-brain.ru |
| Сервер | Hetzner Antigravity-1 (178.104.27.196) |
| Compose | `/root/digital-household/lms-staging/` |
| Build dir | `/root/lms-staging-build/` |
| Порт | 3011 |
| БД | `lms_staging_db` (отдельный контейнер) |
| Email | Отключён (RESEND_API_KEY пустой) |
| S3 | Тот же бакет что и prod |
## Деплой одной командой (с Mac)
```bash
bash ~/Documents/Claude/scripts/deploy-staging.sh
```
## Ручные шаги (если нужно)
```bash
# На сервере
cd /root/lms-staging-build && git pull
docker build -t lms-staging-app:latest .
cd /root/digital-household/lms-staging && docker compose up -d app
docker logs lms-staging-app-1 --tail 20
```
## Откат
```bash
ssh root@178.104.27.196 "cd /root/digital-household/lms-staging && docker compose down"
```
## Восстановить prod-данные в staging (для отладки)
```bash
# 1. Взять свежий дамп с 3TB или Storage Box
# 2. Залить в staging-базу
DUMP="/Volumes/3TB/Second Brain Production/LMS Backups/lms-db-YYYYMMDD-HHMM.sql.gz"
scp "$DUMP" root@178.104.27.196:/tmp/staging-restore.sql.gz
ssh root@178.104.27.196 \
"zcat /tmp/staging-restore.sql.gz | docker exec -i lms-staging-db-1 psql -U lms_staging_user lms_staging_db"
```
## Связанные процессы
- [[Процессы/lms-deploy]] — деплой в prod (после проверки на staging)
- [[Процессы/lms-disaster-recovery]]
```
- [ ] **Шаг 8.4: Коммит скрипта**
```bash
cd ~/Documents/Claude/lms-system
git add docs/
git commit -m "Add LMS staging environment plan and deploy script"
git push origin HEAD
```
---
## Чек-лист финальной проверки
- [ ] `https://staging.school.second-brain.ru/login` → 200
- [ ] Войти под тестовым аккаунтом
- [ ] Курс открывается
- [ ] Email НЕ уходит при регистрации (проверить логи)
- [ ] `bash deploy-staging.sh` завершается с `✅ Staging OK`
- [ ] SBT-заметка создана
---
## Ожидаемое время
~30–40 минут (основное время — сборка Docker-образа ~3–5 мин).