# Инструкция по развёртыванию — МОНТ Инфраструктурный Полигон Версия проекта: **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`