- Woodpecker workflows требуют агент с меткой isolation=dedicated-vm - шаги выполняются в lms-ci:1 с nested dockerd, без хост-Docker сокета - ci/Dockerfile: прогретый immutable-образ (Java, Python, Node, Gradle-кэш) - ci-dind.sh поднимает dockerd внутри privileged-шага; ci-build-image.sh собирает образ - deploy/WOODPECKER.md: отдельная VM, firewall, отсутствие production-секретов, trusted repo, чек-лист безопасности
214 lines
11 KiB
Markdown
214 lines
11 KiB
Markdown
# Деплой LMS на сервер `lms.our-place.ru`
|
||
|
||
Стек целиком запускается в Docker. **Java на сервере не устанавливается** — приложение
|
||
собирается и работает внутри контейнеров (multi-stage `Dockerfile`: сборка на JDK 21,
|
||
запуск на JRE 21).
|
||
|
||
Компоненты:
|
||
|
||
| Сервис | Контейнер | Наружу |
|
||
|---|---|---|
|
||
| web (Thymeleaf UI) | `lms-web` | `127.0.0.1:8090` (проксирует nginx) |
|
||
| gateway | `lms-gateway` | нет |
|
||
| identity / academic / progress | `lms-*` | нет |
|
||
| PostgreSQL 18 | `lms-postgres` | нет |
|
||
|
||
Данные лежат в именованных volume: `lms_pgdata` (БД), `lms_submissions` (решения),
|
||
`lms_assets` (картинки материалов).
|
||
|
||
---
|
||
|
||
## 0. Что нужно
|
||
|
||
- Сервер с Ubuntu 22.04/24.04 (или другим Linux с Docker), sudo/root.
|
||
- Домен `lms.our-place.ru`, A-запись которого указывает на IP сервера.
|
||
- Открытые порты 80 и 443 (443 — если настраиваете HTTPS).
|
||
|
||
## 1. Установка Docker
|
||
|
||
```bash
|
||
curl -fsSL https://get.docker.com | sudo sh
|
||
sudo usermod -aG docker "$USER" # перелогиньтесь, чтобы работать без sudo
|
||
docker compose version # нужна версия v2
|
||
```
|
||
|
||
## 2. Получить код
|
||
|
||
```bash
|
||
sudo mkdir -p /opt/lms && sudo chown "$USER" /opt/lms
|
||
git clone <URL-репозитория> /opt/lms
|
||
cd /opt/lms
|
||
```
|
||
|
||
## 3. Настроить окружение
|
||
|
||
```bash
|
||
cp .env.prod.example .env.prod
|
||
nano .env.prod
|
||
```
|
||
|
||
Обязательно задайте `LMS_DB_PASSWORD` и `LMS_INTERNAL_TOKEN`. Остальные
|
||
значения по умолчанию подходят.
|
||
|
||
`LMS_INTERNAL_TOKEN` — общий секрет для внутренних вызовов identity из
|
||
academic/progress (профиль студента, состав группы). Сгенерируйте длинную
|
||
случайную строку (например, `openssl rand -hex 32`) и укажите её в `.env.prod`.
|
||
Compose передаёт одно и то же значение identity, academic и progress.
|
||
Значение по умолчанию `dev-internal-token` для production не используйте;
|
||
`.env.prod` не коммитьте.
|
||
|
||
Сроки аутентификации и сессии (можно не задавать — действуют значения ниже):
|
||
|
||
```dotenv
|
||
LMS_ACCESS_TOKEN_TTL=15m # access JWT identity, короткий
|
||
LMS_REFRESH_TOKEN_TTL=7d # refresh-токен, ротация и отзыв
|
||
LMS_SESSION_TIMEOUT=7d # web-сессия LMS_SESSION, sliding
|
||
LMS_SESSION_ABSOLUTE_TIMEOUT=30d # жёсткий потолок сессии
|
||
LMS_REFRESH_TOKEN_RETENTION=30d # сколько хранить истёкшие refresh
|
||
LMS_REFRESH_TOKEN_CLEANUP_CRON=0 30 3 * * *
|
||
```
|
||
|
||
`LMS_ACCESS_TOKEN_TTL`, `LMS_REFRESH_TOKEN_TTL`,
|
||
`LMS_SESSION_ABSOLUTE_TIMEOUT`, `LMS_REFRESH_TOKEN_RETENTION` и
|
||
`LMS_REFRESH_TOKEN_CLEANUP_CRON` читает identity; `LMS_SESSION_TIMEOUT` и
|
||
`LMS_SESSION_ABSOLUTE_TIMEOUT` — web. `docker-compose.prod.yml` прокидывает эти
|
||
переменные в environment соответствующих сервисов; при переопределении
|
||
используются значения из `.env.prod`. Access
|
||
обновляется автоматически за 2 минуты до истечения; прямой API-клиент должен
|
||
сам вызывать `POST /api/v1/auth/refresh`. Уменьшайте значения только осознанно:
|
||
слишком короткая web-сессия заставит пользователей чаще входить заново.
|
||
|
||
## 4. Собрать и запустить
|
||
|
||
```bash
|
||
./deploy/deploy.sh up
|
||
```
|
||
|
||
Скрипт соберёт образы (первый раз ~5–15 минут: скачиваются Gradle и зависимости)
|
||
и поднимет контейнеры. Проверка:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml --env-file .env.prod ps
|
||
curl -fsS http://127.0.0.1:8090/actuator/health # {"status":"UP",...}
|
||
```
|
||
|
||
## 5. Nginx + HTTPS
|
||
|
||
```bash
|
||
sudo apt-get update && sudo apt-get install -y nginx certbot python3-certbot-nginx
|
||
sudo mkdir -p /var/www/certbot
|
||
|
||
sudo cp deploy/nginx/lms.our-place.ru.conf /etc/nginx/sites-available/lms.our-place.ru.conf
|
||
sudo ln -sf /etc/nginx/sites-available/lms.our-place.ru.conf /etc/nginx/sites-enabled/lms.our-place.ru.conf
|
||
sudo rm -f /etc/nginx/sites-enabled/default
|
||
|
||
sudo nginx -t && sudo systemctl reload nginx
|
||
sudo certbot --nginx -d lms.our-place.ru --redirect -m admin@our-place.ru --agree-tos
|
||
```
|
||
|
||
`certbot --nginx` сам добавит TLS-блок и редирект с HTTP на HTTPS. Автопродление
|
||
уже настроено пакетом (`systemctl status certbot.timer`).
|
||
|
||
## 6. Первый запуск приложения
|
||
|
||
Откройте `https://lms.our-place.ru` → откроется `/setup` → создайте администратора.
|
||
|
||
## 6a. Наполнение данными
|
||
|
||
Дальше всё через интерфейс: импорт групп (`.xls`), импорт лабораторной ZIP'ом,
|
||
раздача вариантов, оценки и защита, материалы. Подробный порядок с проверками —
|
||
в [`IMPORT.md`](IMPORT.md). Файлы со списками студентов и выгрузками паролей —
|
||
персональные данные, держите их вне репозитория.
|
||
|
||
## 7. Обновление версии
|
||
|
||
```bash
|
||
cd /opt/lms
|
||
git pull
|
||
./deploy/deploy.sh up # backup → сборка → миграции → перезапуск
|
||
```
|
||
|
||
`deploy.sh up` **обязательно** делает backup перед обновлением. Если backup
|
||
завершился ошибкой, деплой прерывается. Миграции БД (Flyway) применяются
|
||
автоматически при старте сервисов: identity `V4–V13` (`V10` создаёт
|
||
`identity.audit_events`, `V11` — `identity.refresh_tokens`, `V12` —
|
||
`absolute_expires_at`, `V13` — keyset-индексы аудита), academic `V6–V12`,
|
||
progress `V13`.
|
||
|
||
После обновления проверьте, что админ открывает `/admin/audit` и видит события
|
||
(например, после входа в профиль студента — `PROFILE_VIEW`); преподаватель и
|
||
студент на эту страницу не попадают. Журнал листается keyset-курсором
|
||
(`cursor`/`limit`, «Показать ещё»; offset `page` остаётся для совместимости).
|
||
Для инвентаризации старых файлов админ открывает `/admin/legacy-assets`,
|
||
вызывает `GET /api/v1/content/legacy-assets` или запускает
|
||
`scripts/legacy-assets.sh`: файлы без привязки к статье больше не публичны
|
||
(`404` для обычных пользователей), их нужно привязать или удалить.
|
||
|
||
CI (`.woodpecker/`, Gitea + Woodpecker 3.x) на каждом push/MR выполняет
|
||
workflow `check` (`./gradlew --no-daemon test ktlintCheck detekt evidenceCheck`)
|
||
и `smoke` (`./scripts/ldd-smoke.sh`: полный стек, health, вход админа, один
|
||
курс). Полный workflow `release` (`./scripts/ldd-release.sh`) обязателен для
|
||
тегов и запускается вручную для веток. Smoke/release начинаются с
|
||
`scripts/ci-preflight.sh`, который проверяет Java 21, Docker Compose v2,
|
||
Python 3, Node и Playwright. Агент — Docker backend на отдельной CI VM,
|
||
шаги — в nested dind без хост-Docker сокета; установка и модель изоляции —
|
||
[`WOODPECKER.md`](WOODPECKER.md).
|
||
|
||
## 8. Резервное копирование
|
||
|
||
Полная инструкция — [`BACKUP.md`](BACKUP.md). Кратко:
|
||
|
||
```bash
|
||
./deploy/backup.sh backup # БД + решения + материалы
|
||
./deploy/backup.sh list
|
||
./deploy/backup.sh verify <dir>
|
||
./deploy/backup.sh restore <dir> # destructive, требует ввода RESTORE
|
||
```
|
||
|
||
Backup включает `pg_dump` БД, volume решений и volume материалов, контрольные
|
||
суммы и manifest. По умолчанию хранится 14 дней (`LMS_BACKUP_KEEP_DAYS`),
|
||
каталог — `/opt/lms/backups` (`LMS_BACKUP_DIR`).
|
||
|
||
Ежедневное расписание:
|
||
|
||
```bash
|
||
sudo cp deploy/systemd/lms-backup.service deploy/systemd/lms-backup.timer /etc/systemd/system/
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now lms-backup.timer
|
||
```
|
||
|
||
Backup на том же сервере — не единственная защита: настройте выгрузку на
|
||
отдельный хост и периодически проверяйте восстановление на staging.
|
||
|
||
## 9. Диагностика
|
||
|
||
```bash
|
||
./deploy/deploy.sh ps
|
||
./deploy/deploy.sh logs web
|
||
./deploy/deploy.sh logs gateway
|
||
./deploy/deploy.sh logs identity
|
||
```
|
||
|
||
Типичные проблемы:
|
||
|
||
- **web отдаёт 502 через nginx** — контейнер `web` не поднялся: смотрите `logs web`.
|
||
Часто причина — занят порт 8090 или не задан `LMS_DB_PASSWORD` (compose не стартует).
|
||
- **Страница входа открывается, но запросы падают** — проверьте `logs gateway` и
|
||
`logs identity`; gateway должен получить JWKS от identity.
|
||
- **401 на страницах после входа (Docker)** — у backend-сервисов не задан адрес
|
||
JWKS: должно быть `LMS_JWT_JWK_SET_URI=http://identity:8081/api/v1/auth/jwks.json`
|
||
(уже прописано в `docker-compose.prod.yml` для academic/progress/gateway).
|
||
Внутри контейнера `localhost:8081` недоступен, поэтому токен не проверяется.
|
||
- **403/502 в профиле студента или составе группы** — проверьте, что
|
||
`LMS_INTERNAL_TOKEN` задан в `.env.prod` и одинаков у identity, academic
|
||
и progress. При неверном внутреннем токене identity отвечает `403`.
|
||
- **Не проходят загрузки больших ZIP** — увеличьте `client_max_body_size` в nginx
|
||
(сейчас 120 МБ) и `spring.servlet.multipart.max-request-size`.
|
||
|
||
## Замечания по безопасности
|
||
|
||
- PostgreSQL наружу не публикуется; связь между контейнерами — по внутренней сети `lms`.
|
||
- Публикуется только `127.0.0.1:8090`, доступ снаружи — исключительно через nginx/TLS.
|
||
- `.env.prod` содержит пароль БД и не коммитится (в `.gitignore`).
|
||
- Пароль администратора задаётся вами на шаге 6; хранение — BCrypt.
|