# Деплой 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 /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 ./deploy/backup.sh restore # 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.