После запуска dev сайт уходил в вечную загрузку без единой ошибки в логе. Открытая вкладка держит SSE-поток /api/events, и сервер его сам не закрывает. При любой правке .py (pull, переключение ветки, мёрж) uvicorn --reload останавливает старый процесс, а тот в graceful shutdown ждёт закрытия всех соединений. Лимита по умолчанию нет, поэтому ожидание длится вечно: новый процесс не стартует, слушающий сокет остаётся у reloader'а, соединения принимаются в backlog и никем не обслуживаются. Запрос /api/users/me висит, RequireAuth крутит спиннер, в логе только "Reloading...". Воспроизведено тем же способом, каким запускает run.ps1 (uvicorn в отдельном окне): при открытом SSE и тронутом .py /api/health не отвечал, хотя TCP-соединение устанавливалось за 12 мс. Сервер ожил ровно в момент закрытия SSE. Теперь uvicorn запускается с --timeout-graceful-shutdown: по истечении лимита он отменяет висящие задачи запросов и доводит перезапуск до конца. В dev лимит 2 с, и тот же сценарий отвечает 200 примерно через 4 с после правки файла. В entrypoint лимит 10 с: в контейнере тот же механизм держал остановку до SIGKILL по stop_grace_period (30 с), и lifespan-shutdown не выполнялся. Команды ручного запуска в README дополнены тем же флагом. #47 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
65 lines
5.5 KiB
Markdown
65 lines
5.5 KiB
Markdown
# Публикация: домены, VPS, туннели
|
||
|
||
Приложение крутится дома (Pi — прод) и на твоём ПК (dev/test). Дома белого 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 │
|
||
│ ▲ контейнер (test) ИЛИ │
|
||
│ ▲ ssh с ПК (dev) │
|
||
│ ├── ПК test : app:8000 │
|
||
│ └── ПК dev : vite:5173 │
|
||
└───────────────────────────────────────────────────┘
|
||
```
|
||
|
||
- **PROD** — Pi. `docker compose up -d` поднимает два сервиса: `app` + `tunnel`. У `app`
|
||
**портов на хост нет** — наружу его выставляет только туннель-контейнер
|
||
(`ssh -R 9000:app:8000` к VPS). Постоянно, Docker сам переподключает. См. [`pi/`](pi/README.md).
|
||
- **TEST** — ПК. То же самое: `docker compose -f docker-compose.test.yml up` поднимает
|
||
`app` + `tunnel` (`ssh -R 9001:app:8000`). Портов на хост нет — тест виден только на
|
||
`forbidden-stars.ru`. Обычно запускается лаунчером при `APP_ENV=test`.
|
||
- **DEV** — ПК, нативно (`uvicorn`+`vite`). По умолчанию только на localhost; при
|
||
`LOCAL_PUBLIC=vps` лаунчер (`run.ps1`) дополнительно поднимает SSH-туннель с ПК
|
||
(`ssh -R 9001:localhost:5173`) → дев виден на `forbidden-stars.ru`.
|
||
- DEV и TEST делят слот **9001** (`forbidden-stars.ru`) → поднимай что-то **одно за раз**.
|
||
PROD на отдельном слоте **9000** (`forbiddenstars.ru`) — работает независимо.
|
||
|
||
Ключ туннеля — **`deploy/tunnel/id_tunnel`** (приватный, в git не идёт). Его публичную
|
||
часть добавь в `authorized_keys` пользователя `tunnel` на VPS. Один и тот же ключ годится
|
||
для контейнерного туннеля (Pi/ПК) и для dev-туннеля `run.ps1`.
|
||
|
||
Настройка по шагам:
|
||
1. **VPS** — [`vps/README.md`](vps/README.md): Caddy, файрвол, пользователь `tunnel`, сертификаты, `Caddyfile`.
|
||
2. **Pi (прод)** — [`pi/README.md`](pi/README.md): ключ в `deploy/tunnel/id_tunnel`, `.env`, `docker compose up -d`.
|
||
3. **ПК (dev/test)** — тот же ключ в `deploy/tunnel/id_tunnel` (для test-контейнера) и/или
|
||
ключ по умолчанию для dev (`run.ps1`); pubkey — в `authorized_keys` у `tunnel@VPS`.
|
||
|
||
Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, `id_tunnel*`) живут на VPS/Pi/ПК,
|
||
в репозитории только `Caddyfile`, `deploy/tunnel/` (образ туннеля) и шаблоны.
|
||
|
||
> 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`
|
||
(`entrypoint.sh`, `run.*`). Без него остановка ждёт закрытия всех соединений: в dev
|
||
`--reload` при открытой вкладке висит вечно, в контейнере остановку обрывает только SIGKILL
|
||
по `stop_grace_period`.
|