# Инструкция по развёртыванию — МОНТ Инфраструктурный Полигон

Версия проекта: **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=<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`
