From b951f6c68e5a111aadab1471f8934655b7f18817 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Tue, 28 Apr 2026 07:08:05 +0000 Subject: [PATCH] docs: add full deployment guide in Russian --- DEPLOY.md | 601 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 601 insertions(+) create mode 100644 DEPLOY.md diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..bb21ac8 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,601 @@ +# Инструкция по развёртыванию — МОНТ Инфраструктурный Полигон + +Версия проекта: **0.6.0** + +--- + +## Содержание + +1. [Что это такое](#1-что-это-такое) +2. [Архитектура](#2-архитектура) +3. [Требования к серверу](#3-требования-к-серверу) +4. [Установка зависимостей](#4-установка-зависимостей) +5. [Клонирование репозитория](#5-клонирование-репозитория) +6. [Настройка переменных окружения](#6-настройка-переменных-окружения) +7. [Настройка Traefik](#7-настройка-traefik) +8. [Сборка Docker-образов рантаймов](#8-сборка-docker-образов-рантаймов) +9. [Первый запуск](#9-первый-запуск) +10. [Инициализация базы данных](#10-инициализация-базы-данных) +11. [Проверка работоспособности](#11-проверка-работоспособности) +12. [Настройка через админку](#12-настройка-через-админку) +13. [Настройка RDP-слотов](#13-настройка-rdp-слотов) +14. [Обновление проекта](#14-обновление-проекта) +15. [Описание всех переменных окружения](#15-описание-всех-переменных-окружения) +16. [Частые проблемы и решения](#16-частые-проблемы-и-решения) + +--- + +## 1. Что это такое + +Веб-портал для выдачи пользователям браузерного доступа к стендам и сервисам. +Поддерживает три типа сервисов: + +| Тип | Описание | +|-----|----------| +| **WEB** | Открывает веб-сайт в браузере Chromium внутри виртуального дисплея (noVNC-стриминг) | +| **VNC** | Подключается по VNC к внешнему хосту | +| **RDP** | Подключается по RDP к внешнему хосту; пул слотов — несколько пользователей одновременно | + +Стек: **FastAPI + PostgreSQL + Traefik + Docker**. + +--- + +## 2. Архитектура + +``` +Интернет + │ HTTPS :443 + ▼ +┌─────────┐ +│ Traefik │ — edge-прокси, TLS, маршрутизация по URL +└────┬────┘ + │ docker network: portal_net + ├──────────────────────────────┐ + ▼ ▼ +┌─────────┐ ┌────────────┐ +│ api │ FastAPI/uvicorn │ maintenance│ (отдельный процесс очистки) +└────┬────┘ └────────────┘ + │ + ├─ /var/run/docker.sock (API управляет контейнерами напрямую) + │ + ▼ +┌──────────┐ +│ db │ PostgreSQL 16 +└──────────┘ + +Динамически создаваемые контейнеры (не в compose): + portal-webpool-N — WEB-сессии (portal-universal-runtime) + portal-universal-N — VNC-сессии (portal-universal-runtime) + portal-rdpslot-SLUG-N — RDP-слоты (portal-rdp-proxy) +``` + +Traefik читает метки (`labels`) у динамически создаваемых контейнеров и автоматически добавляет маршруты без перезапуска. + +--- + +## 3. Требования к серверу + +| Параметр | Минимум | Рекомендовано | +|----------|---------|---------------| +| ОС | Ubuntu 22.04 / Debian 12 | Ubuntu 24.04 | +| CPU | 2 ядра | 4+ ядра | +| RAM | 4 ГБ | 8+ ГБ | +| Диск | 20 ГБ | 40+ ГБ (Docker-образы большие) | +| Docker | 24+ | 29+ | +| Docker Compose | v2.20+ | v2.40+ | +| Внешний IP | Обязателен | — | +| DNS-запись | A-запись домена → IP сервера | — | +| Порты открыты | 80, 443 | — | + +> **Важно**: домен и DNS-запись нужны для автоматического получения TLS-сертификата через Let's Encrypt. +> Без домена — использовать самоподписанный сертификат (см. [раздел 7](#7-настройка-traefik)). + +--- + +## 4. Установка зависимостей + +```bash +# Обновляем систему +sudo apt-get update && sudo apt-get upgrade -y + +# Устанавливаем Docker (официальный способ) +curl -fsSL https://get.docker.com | sh + +# Добавляем текущего пользователя в группу docker (без sudo) +sudo usermod -aG docker $USER +newgrp docker + +# Проверяем +docker --version # Docker version 29.x.x +docker compose version # Docker Compose version 2.x.x +``` + +--- + +## 5. Клонирование репозитория + +Все исходники находятся в git-репозитории: + +``` +https://git.ruslan.xyz/ruslan/Stend_mont +``` + +```bash +# Рабочий каталог — можно любой, например /opt/stand или ~/docker/stand +mkdir -p ~/docker && cd ~/docker + +# Репозиторий приватный — нужны credentials +git clone https://USER:TOKEN@git.ruslan.xyz/ruslan/Stend_mont.git stand +cd stand +``` + +> Запросите логин и токен у владельца репозитория. + +Структура проекта после клонирования: +``` +stand/ +├── app/ # FastAPI-приложение (Dockerfile + main.py + шаблоны) +├── rdp-proxy/ # Docker-образ RDP-рантайма (xvfb + xfreerdp + noVNC) +├── universal-runtime/ # Docker-образ WEB/VNC-рантайма (Chromium + x11vnc + noVNC) +├── kiosk/ # Docker-образ kiosk-режима (опционально) +├── traefik/ # Конфиг Traefik (traefik.yml + dynamic/) +├── scripts/ # SQL-схема БД +├── docker-compose.yml +└── .env.example # Шаблон переменных окружения +``` + +--- + +## 6. Настройка переменных окружения + +Скопируйте шаблон и отредактируйте: + +```bash +cp .env.example .env +nano .env +``` + +**Минимальный набор значений, которые нужно поменять:** + +```dotenv +# Домен, на котором будет работать портал +PUBLIC_HOST=your-domain.example.com + +# Email для Let's Encrypt (уведомления об истечении сертификата) +LETSENCRYPT_EMAIL=admin@example.com + +# Пароль PostgreSQL (придумайте сами, не менее 16 символов) +POSTGRES_PASSWORD=supersecretdbpassword + +# Секретный ключ для подписи сессий (минимум 32 случайных символа) +# Генерация: python3 -c "import secrets; print(secrets.token_hex(32))" +SIGNING_KEY=your_random_signing_key_here + +# Логин и пароль администратора портала +ADMIN_USERNAME=admin +ADMIN_PASSWORD=your_admin_password +``` + +**Остальные переменные** — описание см. в [разделе 15](#15-описание-всех-переменных-окружения). + +--- + +## 7. Настройка Traefik + +### 7.1 Файл `traefik/traefik.yml` + +Откройте и проверьте email в секции `certificatesResolvers`: + +```bash +nano traefik/traefik.yml +``` + +```yaml +certificatesResolvers: + letsencrypt: + acme: + email: admin@example.com # ← ваш email + storage: /letsencrypt/acme.json + tlsChallenge: {} +``` + +Порты по умолчанию: **HTTP :8288**, **HTTPS :2288** (нестандартные, прописаны в `docker-compose.yml`). +Если нужны стандартные 80/443: + +```bash +nano docker-compose.yml +# Найдите секцию traefik -> ports и замените: +# - "0.0.0.0:8288:80" → - "80:80" +# - "0.0.0.0:2288:443" → - "443:443" +``` + +### 7.2 Создание файла хранилища сертификатов + +```bash +mkdir -p traefik/letsencrypt +touch traefik/letsencrypt/acme.json +chmod 600 traefik/letsencrypt/acme.json +``` + +### 7.3 Работа без домена (самоподписанный сертификат) + +Если домена нет и нужен только HTTP или самоподписанный HTTPS: + +В `traefik/traefik.yml` удалите секцию `certificatesResolvers`. + +В `docker-compose.yml` в labels контейнера `api` замените: +```yaml +- traefik.http.routers.portal.entrypoints=websecure +- traefik.http.routers.portal.tls=true +- traefik.http.routers.portal.tls.certresolver=letsencrypt +``` +на: +```yaml +- traefik.http.routers.portal.entrypoints=web +``` + +--- + +## 8. Сборка Docker-образов рантаймов + +Рантаймы — это образы для WEB/VNC/RDP-сессий. Они **не входят** в основной `docker compose up`, их нужно собрать отдельно. + +```bash +cd ~/docker/stand + +# Образ для WEB и VNC сессий (Chromium + x11vnc + noVNC) +docker build -t portal-universal-runtime:latest ./universal-runtime/ + +# Образ для RDP сессий (xvfb + xfreerdp + x11vnc + noVNC) +docker build -t portal-rdp-proxy:latest ./rdp-proxy/ + +# Образ kiosk (опционально, если используется kiosk-режим) +docker build -t portal-kiosk:latest ./kiosk/ +``` + +> Сборка занимает 3–10 минут в зависимости от скорости интернета (скачивается Chromium и другие пакеты). + +Проверьте что образы появились: +```bash +docker images | grep portal +# Должно быть: +# portal-universal-runtime latest ... +# portal-rdp-proxy latest ... +# portal-kiosk latest ... +``` + +--- + +## 9. Первый запуск + +```bash +cd ~/docker/stand + +# Запускаем всё (traefik, db, api, maintenance) +docker compose up -d --build + +# Смотрим логи запуска +docker compose logs -f api +``` + +Дождитесь строки: +``` +INFO: Application startup complete. +``` + +Затем откройте браузер: `https://your-domain.example.com` + +--- + +## 10. Инициализация базы данных + +База данных инициализируется **автоматически** при первом старте API через SQLAlchemy (ORM создаёт все таблицы). + +Если нужно создать таблицы вручную (на случай сбоя): + +```bash +# Подключитесь к контейнеру PostgreSQL +docker exec -it stend_mont-db-1 psql -U portal -d portal + +# Выполните скрипт схемы +\i /dev/stdin +# вставьте содержимое scripts/schema.sql и нажмите Ctrl+D +``` + +Или через файл: +```bash +docker exec -i stend_mont-db-1 psql -U portal -d portal < scripts/schema.sql +``` + +> Таблица `rdp_slots` создаётся автоматически ORM, её нет в `schema.sql` — это нормально. + +--- + +## 11. Проверка работоспособности + +```bash +# Статус всех контейнеров +docker compose ps + +# Логи отдельных сервисов +docker compose logs api # FastAPI +docker compose logs db # PostgreSQL +docker compose logs traefik # Traefik (маршруты, сертификаты) +docker compose logs maintenance # Фоновая очистка сессий + +# Проверка что база доступна +docker exec stend_mont-db-1 pg_isready -U portal + +# Проверка API (изнутри сети) +docker exec stend_mont-api-1 curl -s http://localhost:8000/health +``` + +Ожидаемый вывод `docker compose ps`: +``` +NAME STATUS +stend_mont-api-1 Up +stend_mont-db-1 Up (healthy) +stend_mont-maintenance-1 Up +stend_mont-traefik-1 Up +``` + +--- + +## 12. Настройка через админку + +1. Откройте `https://your-domain.example.com/admin` +2. Войдите с данными `ADMIN_USERNAME` / `ADMIN_PASSWORD` из `.env` + +### Создание пользователей + +**Пользователи → Добавить пользователя:** +- Логин, пароль, срок действия аккаунта +- После создания — назначить права доступа к сервисам (раздел ACL) + +### Создание сервисов + +**Сервисы → Добавить сервис:** + +| Поле | Описание | +|------|----------| +| Название | Отображаемое название | +| Slug | URL-идентификатор (латиница, цифры, дефис) | +| Тип | WEB / VNC / RDP | +| Адрес | `host:port` для VNC/RDP; URL для WEB | +| Иконка | PNG/SVG, загружается через форму | +| Размер пула | Для WEB: количество прогретых контейнеров (0 = по запросу) | + +### Назначение доступа (ACL) + +**ACL → выберите пользователя → отметьте сервисы → Сохранить** + +--- + +## 13. Настройка RDP-слотов + +RDP-сервисы используют **пул слотов**: каждый слот = отдельный RDP-пользователь + отдельный контейнер. +Пользователи занимают свободные слоты, при их нехватке получают ошибку 503. + +### Шаг 1: Создайте RDP-сервис + +В админке создайте сервис с типом **RDP**: +- **Адрес**: `hostname_или_ip:3389` (без протокола) +- Домен, безопасность — если нужны + +### Шаг 2: Добавьте слоты (RDP-пользователей) + +В карточке сервиса появится раздел **«RDP пользователи (слоты пула)»**: +- Введите RDP-логин и пароль пользователя на RDP-сервере +- Нажмите **Добавить** +- Повторите для каждого параллельного пользователя (слота) + +### Шаг 3: Запустите контейнеры слотов + +```bash +# Перезапустите API с флагом инициализации +cd ~/docker/stand +ENABLE_STARTUP_MAINTENANCE=1 docker compose up -d api maintenance +``` + +После запуска появятся контейнеры вида `portal-rdpslot-SLUG-N`. + +Проверка: +```bash +docker ps | grep rdpslot +``` + +### Назначение прав на RDP-сервис + +В разделе ACL назначьте нужным пользователям доступ к RDP-сервису (обычный чекбокс). +Любой пользователь с доступом может занять любой свободный слот. + +--- + +## 14. Обновление проекта + +```bash +cd ~/docker/stand + +# Получаем изменения +git pull + +# Пересобираем API (если менялся main.py или шаблоны) +docker compose up -d --build api maintenance + +# Пересобираем рантаймы (если менялись rdp-proxy/, universal-runtime/, kiosk/) +docker build -t portal-rdp-proxy:latest ./rdp-proxy/ +docker build -t portal-universal-runtime:latest ./universal-runtime/ +docker build -t portal-kiosk:latest ./kiosk/ + +# После пересборки рантаймов — перезапустить слот-контейнеры +# Старые запущенные сессии продолжат работать, новые получат новый образ +docker compose down api maintenance +ENABLE_STARTUP_MAINTENANCE=1 docker compose up -d api maintenance +``` + +> **Важно**: `docker compose up -d api` **без** `--build` НЕ обновит код, если контейнер уже запущен. +> Всегда используйте `--build` после изменений в `app/`. + +--- + +## 15. Описание всех переменных окружения + +| Переменная | По умолчанию | Описание | +|------------|-------------|----------| +| `COMPOSE_PROJECT_NAME` | `stend_mont` | Префикс имён контейнеров Docker | +| `PUBLIC_HOST` | — | **Обязательно.** Домен сайта, напр. `stand.example.com` | +| `LETSENCRYPT_EMAIL` | — | Email для Let's Encrypt | +| `POSTGRES_DB` | `portal` | Имя базы данных | +| `POSTGRES_USER` | `portal` | Пользователь PostgreSQL | +| `POSTGRES_PASSWORD` | — | **Обязательно.** Пароль PostgreSQL | +| `SIGNING_KEY` | — | **Обязательно.** Секрет для подписи сессионных токенов (мин. 32 символа) | +| `ADMIN_USERNAME` | `admin` | Логин администратора | +| `ADMIN_PASSWORD` | — | **Обязательно.** Пароль администратора | +| `ADMIN_TTL_DAYS` | `3650` | Срок действия аккаунта admin (дни) | +| `SESSION_IDLE_SECONDS` | `7200` | Тайм-аут сессии по бездействию (секунды). Рекомендуется `300` (5 мин) | +| `UVICORN_WORKERS` | `6` | Количество воркеров uvicorn | +| `WEB_POOL_SIZE` | `20` | Максимальное число WEB-контейнеров в пуле | +| `WEB_POOL_BUFFER` | `2` | Сколько прогретых WEB-контейнеров держать в запасе | +| `PREWARM_POOL_SIZE` | `2` | Размер пула прогрева для VNC | +| `UNIVERSAL_POOL_SIZE` | `0` | Размер универсального пула | +| `MAX_ACTIVE_SERVICES_PER_USER` | `4` | Максимум одновременных сессий на одного пользователя | +| `ENABLE_STARTUP_MAINTENANCE` | `0` | `1` = при старте запустить/переинициализировать все пул-контейнеры | +| `TRAEFIK_INTERNAL_URL` | `http://traefik` | URL Traefik изнутри Docker-сети (не менять без нужды) | +| `LOG_LEVEL` | `INFO` | Уровень логирования: `DEBUG`, `INFO`, `WARNING`, `ERROR` | +| `LOG_SLOW_REQUEST_MS` | `2000` | Запросы дольше этого (мс) логируются как медленные | +| `GO_USER_LOCK_TIMEOUT_SECONDS` | `8` | Тайм-аут блокировки при запуске сессии пользователем | +| `GO_POOL_LOCK_TIMEOUT_SECONDS` | `20` | Тайм-аут блокировки при захвате слота пула | +| `POOL_DISPATCH_RETRIES` | `6` | Число попыток занять слот пула | +| `POOL_DISPATCH_REQUEST_TIMEOUT_SECONDS` | `2.0` | Тайм-аут одного запроса к пул-контейнеру | +| `POOL_DISPATCH_SLEEP_SECONDS` | `0.3` | Пауза между попытками диспетчеризации | +| `X11VNC_FLAGS` | `-wait 5 -defer 5 -threads` | Дополнительные флаги x11vnc | +| `WEB_RESOLUTION_MIN_WIDTH` | `1024` | Минимальная ширина разрешения для WEB-сессий | +| `WEB_RESOLUTION_MIN_HEIGHT` | `720` | Минимальная высота | +| `WEB_RESOLUTION_MAX_WIDTH` | `3840` | Максимальная ширина | +| `WEB_RESOLUTION_MAX_HEIGHT` | `2160` | Максимальная высота | + +--- + +## 16. Частые проблемы и решения + +### Сертификат не получается (Let's Encrypt) + +**Симптом:** Браузер показывает ошибку TLS, в логах Traefik: `acme: error`. + +**Причины и решения:** +1. DNS не указывает на сервер — проверьте A-запись: `nslookup your-domain.example.com` +2. Порт 80 или 443 закрыт фаерволом — откройте: `sudo ufw allow 80 && sudo ufw allow 443` +3. Файл `acme.json` не имеет прав 600: `chmod 600 traefik/letsencrypt/acme.json` +4. Слишком много запросов к LE — подождите час и попробуйте снова + +--- + +### API не стартует (ошибка подключения к БД) + +**Симптом:** В логах `api`: `connection refused` или `could not connect to server`. + +```bash +# Проверьте что база запущена +docker compose ps db +docker compose logs db + +# Убедитесь что переменные совпадают в .env +grep POSTGRES .env +``` + +Если база не успела подняться — подождите 5–10 секунд и перезапустите API: +```bash +docker compose restart api +``` + +--- + +### RDP-сессия не подключается (чёрный экран) + +**Симптом:** noVNC открывается, но экран чёрный или появляется ошибка. + +```bash +# Найдите имя контейнера слота +docker ps | grep rdpslot + +# Смотрите логи внутри контейнера +docker exec portal-rdpslot-SLUG-N cat /tmp/xfreerdp.log +docker exec portal-rdpslot-SLUG-N cat /tmp/xvfb.log +docker exec portal-rdpslot-SLUG-N cat /tmp/x11vnc.log +``` + +Типичные ошибки: + +| Ошибка | Причина | Решение | +|--------|---------|---------| +| `Server is already active for display :1` | Старый lock-файл Xvfb | Обновите `rdp-proxy/entrypoint.sh` (уже исправлено в v0.6.0) | +| `Authentication failure` | Неверный RDP логин/пароль | Проверьте слот в админке | +| `failed to open display :1` | Xvfb не запустился | Перезапустите контейнер слота | +| `Connection refused` к `:3389` | RDP-сервер недоступен | Проверьте `host:port` сервиса | + +Перезапуск конкретного слота: +```bash +docker restart portal-rdpslot-SLUG-N +``` + +--- + +### Слот показывается свободным, но пользователь всё ещё в сессии + +**Симптом:** В админке слот «Свободен», но пользователь зашёл в /view. + +Это возникает если сессия осталась в статусе `ACTIVE` без обновления `last_access_at`. +Принудительно истечь сессию через PostgreSQL: + +```bash +docker exec -it stend_mont-db-1 psql -U portal -d portal -c \ + "UPDATE sessions SET status='EXPIRED' WHERE status='ACTIVE' AND service_id=;" +``` + +--- + +### Контейнеры пула не создаются при старте + +**Симптом:** После запуска нет контейнеров `portal-rdpslot-*` или `portal-webpool-*`. + +Убедитесь что стартовали с флагом: +```bash +ENABLE_STARTUP_MAINTENANCE=1 docker compose up -d api maintenance +``` + +Или добавьте в `.env`: +```dotenv +ENABLE_STARTUP_MAINTENANCE=1 +``` +и перезапустите: +```bash +docker compose up -d api maintenance +``` + +--- + +### Полная переустановка (сброс данных) + +> **Внимание**: удалятся все пользователи, сессии, сервисы! + +```bash +cd ~/docker/stand +docker compose down -v # -v удаляет volume с данными PostgreSQL +docker compose up -d --build +``` + +--- + +## Итоговый чеклист первого развёртывания + +- [ ] Сервер с Ubuntu 22.04+ и публичным IP +- [ ] DNS A-запись `ваш-домен → IP сервера` +- [ ] Установлен Docker 24+ и Docker Compose v2 +- [ ] Репозиторий склонирован +- [ ] Заполнен `.env` (PUBLIC_HOST, POSTGRES_PASSWORD, SIGNING_KEY, ADMIN_PASSWORD) +- [ ] Создан `traefik/letsencrypt/acme.json` с правами 600 +- [ ] Собраны образы рантаймов (`portal-rdp-proxy`, `portal-universal-runtime`, `portal-kiosk`) +- [ ] Выполнен `docker compose up -d --build` +- [ ] Открывается `https://ваш-домен/admin` → вход в систему +- [ ] Созданы пользователи и сервисы +- [ ] Для RDP: добавлены слоты, перезапущен API с `ENABLE_STARTUP_MAINTENANCE=1`