# Развёртывание на своём сервере (Debian 12 / 13) Сплит-тренер состоит из статических файлов (HTML, CSS, JS) и ничего не хранит на сервере. Нет бэкенда, базы данных, куки и обращений к внешним сайтам. Сервер только отдаёт файлы, поэтому требования минимальные. ## 1. Минимальные требования ### Сервер | | Минимум | Комментарий | |---|---|---| | ОС | Debian 12 (bookworm) или Debian 13 (trixie), amd64 или arm64 | | | CPU / RAM | 1 vCPU, 128 МБ свободной памяти | nginx в простое занимает 5–10 МБ; для контейнера задан лимит 64 МБ | | Диск | порядка 500 МБ с Docker Engine (сам образ занимает несколько десятков МБ) или ~10 МБ с nginx из пакетов | сам сайт меньше 1 МБ | | Сеть | один TCP-порт; по умолчанию `127.0.0.1:8080` | наружу выставляет обратный прокси | | Исходящий доступ | только на время установки (apt или docker pull) | во время работы сайту интернет не нужен | | ПО (вариант A) | Docker Engine 24+ с плагином Compose v2 | рекомендуется официальный репозиторий: docs.docker.com/engine/install/debian | | ПО (вариант B) | nginx 1.22+ из пакетов Debian (в Debian 12 это 1.22, в 13 это 1.26) | ставится скриптом | | Права | root или sudo на время установки | сайт работает без root-прав | Нужно выбрать один вариант: A или B. ### Домен и HTTPS - Для работы сайта HTTPS **не обязателен**: приложение не использует API, требующие защищённого контекста. - Но на публичном адресе HTTPS нужен: его даёт ваш обратный прокси (раздел 4). ### Браузеры пользователей Современные: Chrome и Edge 111+, Firefox 113+, Safari 16.2+ (примерно 2023 год и новее). Для работы нужен включённый JavaScript и `localStorage`. ## 2. Что важно знать про данные - Прогресс и статистика хранятся **в браузере пользователя** (`localStorage`), на сервер ничего не отправляется. Резервировать на сервере нечего. - Данные привязаны к адресу сайта (схема + домен + порт). Если сменить домен, пользователи начнут «с нуля»; смена пути (`/trainer/`) прогресс не затрагивает. - Файл раскладки Vial (.vil) читается локально в браузере и никуда не загружается. ## 3. Установка Склонируйте репозиторий на сервер: ```bash git clone https://gittea.khad1997pav.ru/khadzhibekovpa/split-trainer.git cd split-trainer ``` ### Вариант A: Docker (рекомендуется) ```bash docker compose up -d --build curl -i http://127.0.0.1:8080/healthz # должно ответить 200 и "ok" ``` - Порт по умолчанию открыт **только на localhost**. Изменить: файл `.env` рядом с `docker-compose.yml`, например `PORT=9000` и/или `BIND=0.0.0.0` (откроет наружу: тогда закройте порт файрволом для всех, кроме прокси). - Контейнер работает без root-прав, с файловой системой «только чтение», без лишних возможностей ядра и с лимитом памяти. - Если прокси тоже в Docker, уберите блок `ports:` и подключите сервис к общей сети прокси: он слушает `8080` внутри контейнера. ### Вариант B: nginx из пакетов Debian ```bash sudo ./deploy/install-debian.sh curl -i http://127.0.0.1:8080/healthz ``` Скрипт ставит nginx, копирует файлы в `/var/www/split-trainer` и подключает отдельный конфиг `split-trainer.conf`. Чужие сайты и настройки он не трогает. Параметры (переменные окружения): | Переменная | По умолчанию | Назначение | |---|---|---| | `LISTEN` | `127.0.0.1:8080` | адрес и порт (или только порт) | | `DOMAIN` | `_` | `server_name`, можно несколько через пробел | | `WEBROOT` | `/var/www/split-trainer` | куда копировать файлы | | `FORCE` | `0` | `1`: разрешить запуск не на Debian 12/13 | Без прокси, прямо на порту 80: ```bash sudo LISTEN=80 DOMAIN=trainer.example.com ./deploy/install-debian.sh ``` Если порт 80 уже занят «сайтом по умолчанию» nginx и вы не задали `DOMAIN`, будет открываться страница «Welcome to nginx»: укажите домен или отключите сайт по умолчанию (`sudo rm /etc/nginx/sites-enabled/default && sudo systemctl reload nginx`). ### Обновление ```bash git pull docker compose up -d --build # вариант A # или sudo ./deploy/install-debian.sh # вариант B ``` Браузеры подхватят новую версию сами: сайт отдаётся с `Cache-Control: no-cache`, то есть при каждом открытии версия сверяется по ETag. ## 4. За обратным прокси От прокси ничего особенного не требуется: сайт статический, без WebSocket, без нестандартных заголовков, все ссылки внутри относительные. Достаточно передать запрос на `127.0.0.1:8080`. ### nginx ```nginx server { listen 443 ssl; server_name trainer.example.com; ssl_certificate /etc/letsencrypt/live/trainer.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/trainer.example.com/privkey.pem; # HTTPS завершается здесь, поэтому HSTS должен ставить именно прокси add_header Strict-Transport-Security "max-age=31536000" always; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ### Caddy ``` trainer.example.com { reverse_proxy 127.0.0.1:8080 } ``` HTTPS-сертификат Caddy получит сам. ### Traefik (Docker, метки в `docker-compose.override.yml`) ```yaml services: split-trainer: ports: !reset [] networks: [traefik] labels: - traefik.enable=true - traefik.http.routers.trainer.rule=Host(`trainer.example.com`) - traefik.http.routers.trainer.tls.certresolver=letsencrypt - traefik.http.services.trainer.loadbalancer.server.port=8080 networks: traefik: external: true ``` Имена сети и резолвера замените на свои. Тег `!reset` требует Docker Compose 2.24 или новее. ### Под подпутём (`https://example.com/trainer/`) Приложение можно отдавать из подкаталога: все пути в нём относительные, свой конфиг менять не нужно. Прокси должен убрать префикс (завершающий `/` в `proxy_pass` обязателен): ```nginx location = /trainer { return 301 /trainer/; } location /trainer/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; } ``` Для Caddy: ``` example.com { redir /trainer /trainer/ handle_path /trainer/* { reverse_proxy 127.0.0.1:8080 } } ``` ### Настоящий IP клиента в логах Конфиг доверяет заголовку `X-Forwarded-For` только от прокси из частных сетей и localhost (`127.0.0.0/8`, `10/8`, `172.16/12`, `192.168/16`). Если ваш прокси стоит на другом сервере с публичным адресом, добавьте его в `set_real_ip_from` в `deploy/split-trainer.conf` и пересоберите или переустановите. ## 5. Что отдаёт сервер - `GET /healthz` отвечает `200 ok` (для мониторинга и healthcheck). - Заголовки безопасности: строгий `Content-Security-Policy` (только свои скрипты и стили, никаких внешних адресов), `X-Content-Type-Options`, `Referrer-Policy: no-referrer`, `X-Frame-Options`, `Permissions-Policy`. - Файлы сжимаются gzip, скрытые файлы (`.git`, `.env`) не отдаются, версия nginx в ответах скрыта. ## 6. Если что-то не работает | Симптом | Причина и решение | |---|---| | 502 / 504 от прокси | Сайт не запущен или слушает другой порт: `docker compose ps`, `curl http://127.0.0.1:8080/healthz` | | Белая страница под `/trainer/` | Нет завершающего `/` в `proxy_pass` или нет редиректа `/trainer` на `/trainer/` (см. выше) | | После обновления видна старая версия | Обновите страницу с Ctrl+F5; если стоит CDN или кеш на прокси, исключите сайт из кеширования | | Прогресс пропал после смены адреса | `localStorage` привязан к адресу сайта (схема + домен + порт); вернуть старый адрес | | `nginx -t` ругается (вариант B) | Покажите вывод `sudo nginx -t`; чаще всего порт занят другим сайтом | | Нужно изменить текст упражнений | Правьте `public/data.js` и обновите сайт командой из раздела «Обновление» |