# Публикация: домены, VPS, туннели Приложение крутится дома (Pi — прод) и на твоём ПК (dev). Дома белого IP нет (CGNAT), поэтому наружу выставляем через **VPS-привратник**: на нём Caddy терминирует HTTPS твоими сертификатами и проксирует трафик в SSH reverse-туннели, которые приложение само открывает к VPS. ``` ┌──────────── VPS 186.246.51.17 (Caddy) ───────────┐ forbiddenstars.ru ───►│ :443 (cert твой) → 127.0.0.1:9000 │ │ ▲ туннель-КОНТЕЙНЕР │ │ └── Pi : app:8000 PROD │ forbidden-stars.ru ──►│ :443 (cert твой) → 127.0.0.1:9001 │ │ ▲ ssh с ПК (по требованию) │ │ └── ПК dev : vite:5173 DEV │ └───────────────────────────────────────────────────┘ ``` - **PROD** — Pi. `docker compose up -d` поднимает три сервиса: `app` + `tunnel` + `backup` (образы из Gitea-реестра, собираются на ПК `scripts/build-push.ps1`). У `app` **портов на хост нет** — наружу его выставляет только туннель-контейнер (`ssh -R 9000:app:8000` к VPS). Работает постоянно: при обрыве `ssh` завершается, и Docker перезапускает контейнер (`restart: unless-stopped`). См. [`pi/`](pi/README.md). - **DEV** — ПК, нативно (`uvicorn`+`vite`). По умолчанию только на localhost; при `LOCAL_PUBLIC=vps` лаунчер (`run.ps1` / `run.sh`) дополнительно поднимает SSH-туннель с ПК (`ssh -R 9001:localhost:5173`) → дев виден на `forbidden-stars.ru` (слот **9001**). - PROD на отдельном слоте **9000** (`forbiddenstars.ru`) — работает независимо от dev. Временный прод на ПК (`docker-compose.temp.yml`) тоже занимает **9000** — одновременно с Pi нельзя. Ключи туннеля (приватные, в git не идут; публичные части — в `authorized_keys` пользователя `tunnel` на VPS): - **Pi** — `TUNNEL_KEY_B64` (base64 приватного ключа) в `.env`; файла ключа на Pi нет. - **ПК, временный прод** — файл `deploy/tunnel/id_tunnel`, монтируется в туннель-контейнер. - **ПК, dev** — `run.ps1`/`run.sh` зовут системный `ssh` без `-i`, то есть с ключом по умолчанию из `~/.ssh`. Он должен быть в `authorized_keys` (можно тем же, что `id_tunnel`). Настройка по шагам: 1. **VPS** — [`vps/README.md`](vps/README.md): Caddy, файрвол, пользователь `tunnel`, сертификаты, `Caddyfile`. 2. **Pi (прод)** — [`pi/README.md`](pi/README.md): ключ туннеля в `TUNNEL_KEY_B64`, `.env`, `docker compose up -d`. 3. **ПК (dev)** — ключ по умолчанию в `~/.ssh` (для dev-туннеля) и, если нужен временный прод, файл `deploy/tunnel/id_tunnel`; pubkey — в `authorized_keys` у `tunnel@VPS`. 4. **Бэкапы** — [`backup/README.md`](backup/README.md): контейнер `backup` (restic) делает снимки на Pi и на VPS (`fsbackup@VPS`, только SFTP), скрипты ПК скачивают их на ПК. Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, `id_tunnel*`, `id_backup*`) живут на VPS/Pi/ПК, в репозитории только `Caddyfile`, страница-заглушка, образы `deploy/tunnel/` и `deploy/backup/` и шаблоны. > Telegram-вход требует HTTPS-домен: у BotFather `/setdomain` укажи оба домена > (`forbiddenstars.ru` и `forbidden-stars.ru`). ## Реал-тайм (SSE) Приложение шлёт события «данные изменились» через SSE: `GET /api/events` (`text/event-stream`). Это обычный HTTP — проходит через Caddy и SSH-туннель без доп. настроек, КРОМЕ одного: для пути `/api/events` в `Caddyfile` выделен отдельный `handle` **без `encode`** и с **`flush_interval -1`** (иначе сжатие/буферизация задержат поток). После правки `Caddyfile` на VPS: `sudo systemctl reload caddy` (или `restart`). Шина событий — **in-memory**, рассчитана на один процесс (uvicorn `--workers 1`, как в контейнере). Если когда-нибудь поднимешь несколько воркеров/реплик — шину нужно вынести во внешний брокер (Redis pub/sub), иначе события увидит только тот воркер, что принял мутацию. SSE-поток сам не закрывается, поэтому uvicorn запускается с `--timeout-graceful-shutdown` (10 с в `entrypoint.sh`, 2 с в `run.*`). Без него остановка ждёт закрытия всех соединений: в dev `--reload` при открытой вкладке висит вечно, в контейнере остановку обрывает только SIGKILL по `stop_grace_period`.