Завершена настройка домена для дева, теста и прода. Соединение через ssh-туннель
This commit is contained in:
+21
-9
@@ -1,8 +1,8 @@
|
|||||||
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
# ║ Forbidden Stars — единый .env (dev и prod) ║
|
# Forbidden Stars — единый .env (dev / test / prod)
|
||||||
# ║ Скопируйте в `.env`, заполните секреты. Реальный `.env` в git НЕ идёт. ║
|
# Скопируйте в .env, заполните секреты. Реальный .env в git НЕ идёт.
|
||||||
# ║ Переключение dev/prod — одной строкой APP_ENV (ниже). ║
|
# Окружение — строкой APP_ENV (ниже); публикация локалки наружу — LOCAL_PUBLIC.
|
||||||
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
# ─── ГЛАВНЫЙ ПЕРЕКЛЮЧАТЕЛЬ ────────────────────────────────────────────────────
|
# ─── ГЛАВНЫЙ ПЕРЕКЛЮЧАТЕЛЬ ────────────────────────────────────────────────────
|
||||||
# Этот параметр читает ЛАУНЧЕР (run.ps1 / run.sh) и решает, что запускать:
|
# Этот параметр читает ЛАУНЧЕР (run.ps1 / run.sh) и решает, что запускать:
|
||||||
@@ -12,6 +12,16 @@
|
|||||||
# Прод-контейнер ИГНОРИРУЕТ это значение и всегда production.
|
# Прод-контейнер ИГНОРИРУЕТ это значение и всегда production.
|
||||||
APP_ENV=development
|
APP_ENV=development
|
||||||
|
|
||||||
|
# ─── ПУБЛИКАЦИЯ ЛОКАЛКИ ЧЕРЕЗ ДОМЕН (VPS-туннель) ─────────────────────────────
|
||||||
|
# Только для dev/test на твоём ПК. local — приложение лишь на localhost; vps —
|
||||||
|
# лаунчер дополнительно поднимает SSH-туннель на VPS, и dev/test виден снаружи на
|
||||||
|
# https://forbidden-stars.ru. Прод (Pi) сюда не относится — у него свой autossh-туннель.
|
||||||
|
LOCAL_PUBLIC=local
|
||||||
|
# Параметры туннеля (нужны только при LOCAL_PUBLIC=vps):
|
||||||
|
VPS_TUNNEL_HOST=186.246.51.17
|
||||||
|
VPS_TUNNEL_USER=tunnel
|
||||||
|
VPS_TUNNEL_PORT=9001 # порт VPS, который слушает Caddy для forbidden-stars.ru
|
||||||
|
|
||||||
# ─── АДМИНИСТРАТОР (вход по логину/паролю, отдельно от Telegram) ──────────────
|
# ─── АДМИНИСТРАТОР (вход по логину/паролю, отдельно от Telegram) ──────────────
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
ADMIN_PASSWORD=change-me-admin-password
|
ADMIN_PASSWORD=change-me-admin-password
|
||||||
@@ -19,10 +29,12 @@ ADMIN_BOOTSTRAP_ENABLED=true
|
|||||||
|
|
||||||
# ─── АУТЕНТИФИКАЦИЯ ИГРОКОВ ───────────────────────────────────────────────────
|
# ─── АУТЕНТИФИКАЦИЯ ИГРОКОВ ───────────────────────────────────────────────────
|
||||||
# Методы входа задаёт APP_ENV: dev → Telegram + stub (вход по нику), prod → только
|
# Методы входа задаёт APP_ENV: dev → Telegram + stub (вход по нику), prod → только
|
||||||
# Telegram. Отдельного переключателя нет. Для Telegram нужны токен и юзернейм бота
|
# Telegram. Для Telegram нужны токен и юзернейм бота (@BotFather). /setdomain у
|
||||||
# (@BotFather) и /setdomain на ваш HTTPS-домен.
|
# BotFather укажи на ОБА домена, где открывается виджет: forbiddenstars.ru (prod)
|
||||||
|
# и forbidden-stars.ru (dev/test).
|
||||||
TELEGRAM_BOT_TOKEN=
|
TELEGRAM_BOT_TOKEN=
|
||||||
TELEGRAM_BOT_USERNAME=
|
TELEGRAM_BOT_USERNAME=
|
||||||
|
# Внешний адрес (зарезервировано): prod https://forbiddenstars.ru; dev/test https://forbidden-stars.ru.
|
||||||
PUBLIC_BASE_URL=
|
PUBLIC_BASE_URL=
|
||||||
|
|
||||||
# ─── БЕЗОПАСНОСТЬ / СЕССИИ ────────────────────────────────────────────────────
|
# ─── БЕЗОПАСНОСТЬ / СЕССИИ ────────────────────────────────────────────────────
|
||||||
@@ -33,8 +45,8 @@ SECRET_KEY=change-me-dev-secret-not-for-production
|
|||||||
JWT_ALGORITHM=HS256
|
JWT_ALGORITHM=HS256
|
||||||
JWT_USER_TTL_MINUTES=10080
|
JWT_USER_TTL_MINUTES=10080
|
||||||
JWT_ADMIN_TTL_MINUTES=480
|
JWT_ADMIN_TTL_MINUTES=480
|
||||||
# dev/test (локально, по HTTP) → false; production за HTTPS на Pi → true.
|
# COOKIE_SECURE задаётся АВТОМАТИЧЕСКИ по окружению (HTTPS-домен ⇒ Secure-cookie):
|
||||||
COOKIE_SECURE=false
|
# dev+localhost → false; dev через VPS, test, prod → true. Вручную задавать НЕ нужно.
|
||||||
COOKIE_DOMAIN=
|
COOKIE_DOMAIN=
|
||||||
|
|
||||||
# ─── БАЗА ДАННЫХ (структура общая, файлы РАЗНЫЕ; выбор по APP_ENV) ────────────
|
# ─── БАЗА ДАННЫХ (структура общая, файлы РАЗНЫЕ; выбор по APP_ENV) ────────────
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ backend/dev.db*
|
|||||||
.env
|
.env
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
# Сертификаты и приватные ключи (живут на VPS/Pi/ПК, не в репозитории)
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
id_tunnel*
|
||||||
|
deploy/**/certs/
|
||||||
|
|
||||||
# Node / фронт
|
# Node / фронт
|
||||||
node_modules/
|
node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
|
|||||||
@@ -144,9 +144,9 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
|||||||
|
|
||||||
**Настройка Telegram (когда будете подключать реальный вход):**
|
**Настройка Telegram (когда будете подключать реальный вход):**
|
||||||
1. Создать бота у [@BotFather](https://t.me/BotFather) → получить **токен** и **username**.
|
1. Создать бота у [@BotFather](https://t.me/BotFather) → получить **токен** и **username**.
|
||||||
2. `/setdomain` у BotFather → ваш HTTPS-домен (где открывается приложение).
|
2. `/setdomain` у BotFather → оба домена: `forbiddenstars.ru` (prod) и `forbidden-stars.ru` (dev/test).
|
||||||
3. В `.env`: `TELEGRAM_BOT_TOKEN=...`, `TELEGRAM_BOT_USERNAME=...` (без `@`).
|
3. В `.env`: `TELEGRAM_BOT_TOKEN=...`, `TELEGRAM_BOT_USERNAME=...` (без `@`).
|
||||||
4. Домену нужен HTTPS (например, Cloudflare Tunnel) — виджет не работает по голому HTTP.
|
4. Виджету нужен HTTPS-домен (см. «Домен и публикация») — по голому HTTP/localhost он не работает.
|
||||||
|
|
||||||
Админ-вход (секретная панель, логин+пароль) — отдельный механизм, доступен во всех окружениях.
|
Админ-вход (секретная панель, логин+пароль) — отдельный механизм, доступен во всех окружениях.
|
||||||
|
|
||||||
@@ -184,6 +184,24 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
|||||||
|
|
||||||
Секреты (`.env`) и данные (`data/`, `*.db`) в git не идут — см. `.gitignore`.
|
Секреты (`.env`) и данные (`data/`, `*.db`) в git не идут — см. `.gitignore`.
|
||||||
|
|
||||||
|
## Домен и публикация
|
||||||
|
|
||||||
|
Публичные адреса отдаёт **VPS-привратник** (Caddy + HTTPS твоими сертификатами), а
|
||||||
|
приложение само открывает к нему SSH-туннель (дома белого IP нет — CGNAT):
|
||||||
|
|
||||||
|
| | домен | источник | туннель |
|
||||||
|
|---|---|---|---|
|
||||||
|
| prod | `forbiddenstars.ru` | Pi, `app:8000` | autossh, постоянно (VPS:9000) |
|
||||||
|
| dev/test | `forbidden-stars.ru` | ПК, vite:5173 / app:8080 | `ssh -R`, по требованию (VPS:9001) |
|
||||||
|
|
||||||
|
- Прод и dev/test на **разных доменах** → могут работать одновременно. Dev и test делят
|
||||||
|
слот `9001` — по очереди.
|
||||||
|
- Публикация локалки: в `.env` поставить `LOCAL_PUBLIC=vps` и запустить `run.ps1` — лаунчер
|
||||||
|
сам поднимет SSH-туннель. `LOCAL_PUBLIC=local` — только localhost.
|
||||||
|
- `COOKIE_SECURE` выводится автоматически (HTTPS-домен ⇒ Secure-cookie; dev+localhost ⇒ нет).
|
||||||
|
- Пошаговая настройка — в [`deploy/`](deploy/README.md): `vps/` (Caddy, сертификаты, туннель-юзер),
|
||||||
|
`pi/` (Docker-приложение + autossh-служба).
|
||||||
|
|
||||||
## Дополнения и фракции
|
## Дополнения и фракции
|
||||||
|
|
||||||
| Дополнение | Фракции |
|
| Дополнение | Фракции |
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ class Settings(BaseSettings):
|
|||||||
app_env: str = "development"
|
app_env: str = "development"
|
||||||
log_level: str = "INFO"
|
log_level: str = "INFO"
|
||||||
|
|
||||||
|
# Публикация локального окружения (dev/test) наружу через VPS-туннель.
|
||||||
|
# Читает ЛАУНЧЕР (run.ps1/run.sh): local — только localhost; vps — плюс SSH-туннель
|
||||||
|
# на forbidden-stars.ru. Влияет на cookie_secure (vps ⇒ снаружи HTTPS ⇒ Secure-cookie).
|
||||||
|
local_public: str = "local"
|
||||||
|
|
||||||
# Часовой пояс приложения (фиксированное смещение, по умолчанию МСК +3).
|
# Часовой пояс приложения (фиксированное смещение, по умолчанию МСК +3).
|
||||||
# Хранение всегда в UTC; смещение применяется к «дате игры» и отображению.
|
# Хранение всегда в UTC; смещение применяется к «дате игры» и отображению.
|
||||||
app_tz_offset_hours: int = 3
|
app_tz_offset_hours: int = 3
|
||||||
@@ -45,7 +50,7 @@ class Settings(BaseSettings):
|
|||||||
jwt_algorithm: str = "HS256"
|
jwt_algorithm: str = "HS256"
|
||||||
jwt_user_ttl_minutes: int = 60 * 24 * 7
|
jwt_user_ttl_minutes: int = 60 * 24 * 7
|
||||||
jwt_admin_ttl_minutes: int = 60 * 8
|
jwt_admin_ttl_minutes: int = 60 * 8
|
||||||
cookie_secure: bool = False
|
# cookie_secure НЕ задаётся вручную — выводится из окружения (см. property ниже).
|
||||||
cookie_domain: str | None = None
|
cookie_domain: str | None = None
|
||||||
|
|
||||||
# Аутентификация. Методы входа определяются окружением (dev: telegram+stub,
|
# Аутентификация. Методы входа определяются окружением (dev: telegram+stub,
|
||||||
@@ -92,6 +97,12 @@ class Settings(BaseSettings):
|
|||||||
def upload_dir(self) -> str:
|
def upload_dir(self) -> str:
|
||||||
return self.dev_upload_dir if self.is_development else self.prod_upload_dir
|
return self.dev_upload_dir if self.is_development else self.prod_upload_dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cookie_secure(self) -> bool:
|
||||||
|
"""Secure-cookie нужен везде, где снаружи HTTPS (домен). Исключение —
|
||||||
|
нативный dev на localhost по HTTP (development + local_public=local)."""
|
||||||
|
return not (self.is_development and self.local_public.lower() == "local")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookie_domain_value(self) -> str | None:
|
def cookie_domain_value(self) -> str | None:
|
||||||
return self.cookie_domain or None
|
return self.cookie_domain or None
|
||||||
|
|||||||
+3
-1
@@ -77,7 +77,9 @@ def create_app() -> FastAPI:
|
|||||||
lifespan=_lifespan,
|
lifespan=_lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.cors_origins_list:
|
# CORS нужен только в dev (vite на :5173 и API на :8000 — разные origin).
|
||||||
|
# В test/prod (и dev через VPS-туннель) всё single-origin → CORS не подключаем.
|
||||||
|
if settings.is_development and settings.cors_origins_list:
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=settings.cors_origins_list,
|
allow_origins=settings.cors_origins_list,
|
||||||
|
|||||||
@@ -8,4 +8,7 @@ echo "[entrypoint] Бутстрап справочников и админист
|
|||||||
python -m app.bootstrap
|
python -m app.bootstrap
|
||||||
|
|
||||||
echo "[entrypoint] Запуск сервера…"
|
echo "[entrypoint] Запуск сервера…"
|
||||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1
|
# --proxy-headers + доверие к X-Forwarded-* от реверс-прокси (Caddy на VPS):
|
||||||
|
# чтобы приложение знало, что снаружи запрос пришёл по HTTPS.
|
||||||
|
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1 \
|
||||||
|
--proxy-headers --forwarded-allow-ips="*"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Публикация: домены, 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 │
|
||||||
|
│ ▲ autossh (постоянно) │
|
||||||
|
│ └── Pi : app:8000 PROD │
|
||||||
|
forbidden-stars.ru ──►│ :443 (cert твой) → 127.0.0.1:9001 │
|
||||||
|
│ ▲ ssh (по требованию) │
|
||||||
|
│ └── ПК : 5173(dev)/8080(test)│
|
||||||
|
└───────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **PROD** — Pi, `forbiddenstars.ru`, туннель постоянный (autossh-служба). См. [`pi/`](pi/README.md).
|
||||||
|
- **DEV/TEST** — ПК, `forbidden-stars.ru`, туннель по требованию: в `.env` поставь
|
||||||
|
`LOCAL_PUBLIC=vps` и запусти `run.ps1` (dev → туннель на vite:5173; test → на :8080).
|
||||||
|
Dev и test делят порт `9001` — по очереди.
|
||||||
|
- Разделение окружений сохраняется на уровне приложения (свои `APP_ENV`, БД, контейнеры);
|
||||||
|
домены разные, поэтому прод и dev/test могут работать одновременно.
|
||||||
|
|
||||||
|
Настройка по шагам:
|
||||||
|
1. **VPS** — [`vps/README.md`](vps/README.md): Caddy, файрвол, пользователь `tunnel`, сертификаты, `Caddyfile`.
|
||||||
|
2. **Pi (прод)** — [`pi/README.md`](pi/README.md): Docker-приложение + autossh-служба туннеля.
|
||||||
|
3. **ПК (dev/test)** — ключ для `tunnel@VPS` (`ssh-copy-id`), затем `LOCAL_PUBLIC=vps` + `run.ps1`.
|
||||||
|
|
||||||
|
Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, SSH-ключи) живут на VPS/Pi/ПК,
|
||||||
|
в репозитории только `Caddyfile` и юнит-шаблон.
|
||||||
|
|
||||||
|
> Telegram-вход требует HTTPS-домен: у BotFather `/setdomain` укажи оба домена
|
||||||
|
> (`forbiddenstars.ru` и `forbidden-stars.ru`).
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Raspberry Pi — ПРОД (`forbiddenstars.ru`)
|
||||||
|
|
||||||
|
Pi запускает приложение в Docker и держит постоянный SSH reverse-туннель к VPS.
|
||||||
|
Снаружи Pi недоступен (CGNAT) — публичная точка только VPS, Pi сам стучится наружу.
|
||||||
|
|
||||||
|
## 1. Приложение (Docker)
|
||||||
|
Из ветки `main`:
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
cp .env.example .env # заполни секреты; APP_ENV контейнер форсит в production
|
||||||
|
docker compose up -d --build
|
||||||
|
curl http://localhost:8000/api/health # → {"status":"ok"}
|
||||||
|
```
|
||||||
|
БД на томе `db-data`, миграции и бутстрап админа — автоматически (`entrypoint.sh`).
|
||||||
|
|
||||||
|
## 2. Ключ для туннеля (без пароля)
|
||||||
|
```bash
|
||||||
|
sudo apt -y install autossh
|
||||||
|
ssh-keygen -t ed25519 -f ~/.ssh/id_tunnel -N ""
|
||||||
|
ssh-copy-id -i ~/.ssh/id_tunnel.pub tunnel@186.246.51.17
|
||||||
|
# (или вручную добавь ~/.ssh/id_tunnel.pub в /home/tunnel/.ssh/authorized_keys на VPS)
|
||||||
|
```
|
||||||
|
Проверка туннеля вручную:
|
||||||
|
```bash
|
||||||
|
autossh -M 0 -N -R 9000:localhost:8000 -i ~/.ssh/id_tunnel tunnel@186.246.51.17
|
||||||
|
# в другом окне/браузере открой https://forbiddenstars.ru — должно работать. Ctrl+C.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Туннель как служба (автозапуск + переподключение)
|
||||||
|
```bash
|
||||||
|
sudo cp deploy/pi/forbidden-stars-tunnel.service /etc/systemd/system/
|
||||||
|
sudoedit /etc/systemd/system/forbidden-stars-tunnel.service # поправь User= и путь к ключу
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now forbidden-stars-tunnel
|
||||||
|
systemctl status forbidden-stars-tunnel
|
||||||
|
```
|
||||||
|
|
||||||
|
Готово: `https://forbiddenstars.ru` отдаёт прод. Логи туннеля: `journalctl -u forbidden-stars-tunnel -f`.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# SSH reverse-туннель ПРОДА: VPS:9000 → Pi localhost:8000 (приложение в Docker).
|
||||||
|
# Caddy на VPS отдаёт forbiddenstars.ru через этот туннель.
|
||||||
|
#
|
||||||
|
# Установка:
|
||||||
|
# sudo cp deploy/pi/forbidden-stars-tunnel.service /etc/systemd/system/
|
||||||
|
# # ПОПРАВЬ под себя: User=, путь к ключу (-i ...), при необходимости IP VPS.
|
||||||
|
# sudo systemctl daemon-reload
|
||||||
|
# sudo systemctl enable --now forbidden-stars-tunnel
|
||||||
|
# systemctl status forbidden-stars-tunnel
|
||||||
|
[Unit]
|
||||||
|
Description=Forbidden Stars: SSH reverse tunnel (prod) to VPS
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# Замени pi на своего пользователя Raspberry Pi (и путь к ключу ниже).
|
||||||
|
User=pi
|
||||||
|
Environment=AUTOSSH_GATETIME=0
|
||||||
|
ExecStart=/usr/bin/autossh -M 0 -N \
|
||||||
|
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes \
|
||||||
|
-i /home/pi/.ssh/id_tunnel \
|
||||||
|
-R 9000:localhost:8000 tunnel@186.246.51.17
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Caddy на VPS (186.246.51.17) — единственная публичная точка входа.
|
||||||
|
# Два домена, ОБА с твоими сертификатами; проксируют в SSH-туннели:
|
||||||
|
#
|
||||||
|
# forbiddenstars.ru → 127.0.0.1:9000 ← Pi (autossh, постоянно) PROD
|
||||||
|
# forbidden-stars.ru → 127.0.0.1:9001 ← ПК (по требованию) DEV/TEST
|
||||||
|
#
|
||||||
|
# Caddy сам терминирует TLS (он и есть edge: видит реального клиента), а вниз к
|
||||||
|
# приложению добавляет X-Forwarded-Proto=https / X-Forwarded-For / Host —
|
||||||
|
# приложение это учитывает (uvicorn --proxy-headers). Положи файл в /etc/caddy/Caddyfile.
|
||||||
|
# Сертификаты — см. deploy/vps/README.md (fullchain = leaf + промежуточные одним файлом).
|
||||||
|
|
||||||
|
forbiddenstars.ru {
|
||||||
|
tls /etc/caddy/certs/forbiddenstars.ru/fullchain.pem /etc/caddy/certs/forbiddenstars.ru/privkey.pem
|
||||||
|
encode zstd gzip
|
||||||
|
reverse_proxy 127.0.0.1:9000
|
||||||
|
}
|
||||||
|
|
||||||
|
forbidden-stars.ru {
|
||||||
|
tls /etc/caddy/certs/forbidden-stars.ru/fullchain.pem /etc/caddy/certs/forbidden-stars.ru/privkey.pem
|
||||||
|
encode zstd gzip
|
||||||
|
reverse_proxy 127.0.0.1:9001
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# VPS (186.246.51.17) — реверс-прокси Caddy + точка входа SSH-туннелей
|
||||||
|
|
||||||
|
Единственная публичная точка. На VPS: Caddy терминирует HTTPS твоими сертификатами
|
||||||
|
для двух доменов и проксирует трафик в SSH reverse-туннели от Pi (прод) и ПК (dev/test).
|
||||||
|
|
||||||
|
```
|
||||||
|
forbiddenstars.ru → 127.0.0.1:9000 ← Pi (autossh, постоянно) PROD
|
||||||
|
forbidden-stars.ru → 127.0.0.1:9001 ← ПК (по требованию) DEV/TEST
|
||||||
|
```
|
||||||
|
|
||||||
|
> Туннель `ssh -R` по умолчанию слушает на loopback VPS (127.0.0.1) — ровно туда смотрит
|
||||||
|
> Caddy. `GatewayPorts` включать НЕ нужно. Снаружи порты 9000/9001 недоступны.
|
||||||
|
|
||||||
|
## 0. DNS
|
||||||
|
A-записи обоих доменов → `186.246.51.17`:
|
||||||
|
```
|
||||||
|
forbiddenstars.ru A 186.246.51.17
|
||||||
|
forbidden-stars.ru A 186.246.51.17
|
||||||
|
```
|
||||||
|
Проверка: `dig +short forbiddenstars.ru` (и второго) должны вернуть этот IP.
|
||||||
|
|
||||||
|
## 1. Зайти на VPS
|
||||||
|
```bash
|
||||||
|
ssh root@186.246.51.17 # пароль введёшь сам
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Обновление + файрвол
|
||||||
|
```bash
|
||||||
|
apt update && apt -y upgrade
|
||||||
|
apt -y install ufw curl gnupg
|
||||||
|
ufw allow OpenSSH
|
||||||
|
ufw allow 80/tcp
|
||||||
|
ufw allow 443/tcp
|
||||||
|
ufw --force enable
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Установить Caddy (официальный репозиторий)
|
||||||
|
```bash
|
||||||
|
apt -y install debian-keyring debian-archive-keyring apt-transport-https
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
||||||
|
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||||||
|
| tee /etc/apt/sources.list.d/caddy-stable.list
|
||||||
|
apt update && apt -y install caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Пользователь под туннели
|
||||||
|
```bash
|
||||||
|
useradd -m -s /bin/bash tunnel
|
||||||
|
install -d -m 700 -o tunnel -g tunnel /home/tunnel/.ssh
|
||||||
|
install -m 600 -o tunnel -g tunnel /dev/null /home/tunnel/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
Публичные ключи Pi и ПК добавишь в `/home/tunnel/.ssh/authorized_keys` на шагах настройки
|
||||||
|
Pi (`deploy/pi/README.md`) и ПК (через `ssh-copy-id`).
|
||||||
|
|
||||||
|
## 5. Сертификаты
|
||||||
|
```bash
|
||||||
|
mkdir -p /etc/caddy/certs/forbiddenstars.ru /etc/caddy/certs/forbidden-stars.ru
|
||||||
|
```
|
||||||
|
Залей свои файлы с локальной машины (с каждого домена — свой `fullchain.pem` + `privkey.pem`):
|
||||||
|
```bash
|
||||||
|
# на локальной машине:
|
||||||
|
scp fullchain.pem privkey.pem root@186.246.51.17:/etc/caddy/certs/forbiddenstars.ru/
|
||||||
|
scp fullchain.pem privkey.pem root@186.246.51.17:/etc/caddy/certs/forbidden-stars.ru/
|
||||||
|
```
|
||||||
|
- `fullchain.pem` = твой сертификат + промежуточные GlobalSign **одним файлом** (сначала leaf, затем chain).
|
||||||
|
- `privkey.pem` = приватный ключ (без пароля).
|
||||||
|
```bash
|
||||||
|
# на VPS — права:
|
||||||
|
chown -R caddy:caddy /etc/caddy/certs && chmod -R 640 /etc/caddy/certs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Caddyfile
|
||||||
|
Скопируй `deploy/vps/Caddyfile` из репозитория в `/etc/caddy/Caddyfile`, затем:
|
||||||
|
```bash
|
||||||
|
caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
systemctl reload caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Проверка
|
||||||
|
1. Подними туннель прода на Pi (см. `deploy/pi/README.md`) и/или dev/test на ПК (`run.ps1` при `LOCAL_PUBLIC=vps`).
|
||||||
|
2. Открой `https://forbiddenstars.ru` и `https://forbidden-stars.ru`.
|
||||||
|
3. Пока соответствующий туннель не поднят — Caddy отдаёт `502`, это ожидаемо.
|
||||||
|
|
||||||
|
Логи Caddy: `journalctl -u caddy -f`.
|
||||||
@@ -11,6 +11,10 @@ export default defineConfig({
|
|||||||
// доступен и из локальной сети (для разработки на своей машине это нормально).
|
// доступен и из локальной сети (для разработки на своей машине это нормально).
|
||||||
host: true,
|
host: true,
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
// Разрешаем хост домена для режима LOCAL_PUBLIC=vps (dev виден на forbidden-stars.ru
|
||||||
|
// через SSH-туннель). localhost/127.0.0.1 vite пропускает по умолчанию.
|
||||||
|
// HMR через домен может не подключиться — это ок, само приложение работает.
|
||||||
|
allowedHosts: ["forbidden-stars.ru"],
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
// Явный IPv4: на Windows "localhost" может резолвиться в IPv6 ::1,
|
// Явный IPv4: на Windows "localhost" может резолвиться в IPv6 ::1,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
#!/usr/bin/env pwsh
|
#!/usr/bin/env pwsh
|
||||||
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
||||||
# ║ Единый лаунчер dev / test. Что запускать — решает APP_ENV из корневого .env ║
|
# ║ Единый лаунчер dev / test. APP_ENV из корневого .env решает, что запускать. ║
|
||||||
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
||||||
# development → uvicorn --reload (бэк) + vite (фронт), нативно, в двух окнах
|
# development → uvicorn --reload (бэк) + vite (фронт), нативно, в двух окнах
|
||||||
# test → docker compose прод-клон (порт 8080)
|
# test → docker compose прод-клон (порт 8080)
|
||||||
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
|
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
|
||||||
#
|
#
|
||||||
|
# LOCAL_PUBLIC=vps дополнительно поднимает SSH-туннель на VPS, и dev/test становится
|
||||||
|
# виден снаружи на https://forbidden-stars.ru (нужен настроенный VPS, см. deploy/vps).
|
||||||
|
#
|
||||||
# Запуск: .\run.ps1
|
# Запуск: .\run.ps1
|
||||||
# Если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
# Если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -17,12 +20,38 @@ if (-not (Test-Path $envFile)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Читаем APP_ENV из .env (последнее присвоение побеждает).
|
# Достаём значение ключа из .env (последнее присвоение побеждает; комментарии игнор).
|
||||||
$appEnv = "development"
|
function Get-EnvVal([string]$name, [string]$default) {
|
||||||
|
$val = $default
|
||||||
foreach ($line in Get-Content $envFile) {
|
foreach ($line in Get-Content $envFile) {
|
||||||
if ($line -match '^\s*APP_ENV\s*=\s*([^#\s]+)') { $appEnv = $matches[1].Trim().ToLower() }
|
if ($line -match "^\s*$name\s*=\s*([^#\s]+)") { $val = $matches[1].Trim() }
|
||||||
|
}
|
||||||
|
return $val
|
||||||
|
}
|
||||||
|
|
||||||
|
$appEnv = (Get-EnvVal "APP_ENV" "development").ToLower()
|
||||||
|
$localPublic = (Get-EnvVal "LOCAL_PUBLIC" "local").ToLower()
|
||||||
|
$vpsHost = Get-EnvVal "VPS_TUNNEL_HOST" ""
|
||||||
|
$vpsUser = Get-EnvVal "VPS_TUNNEL_USER" "tunnel"
|
||||||
|
$vpsPort = Get-EnvVal "VPS_TUNNEL_PORT" "9001"
|
||||||
|
|
||||||
|
Write-Host "APP_ENV = $appEnv LOCAL_PUBLIC = $localPublic" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Поднимает SSH reverse-tunnel: VPS:$vpsPort → localhost:$localPort (в отдельном окне).
|
||||||
|
function Start-VpsTunnel([int]$localPort) {
|
||||||
|
if (-not $vpsHost) {
|
||||||
|
Write-Host "LOCAL_PUBLIC=vps, но VPS_TUNNEL_HOST не задан в .env — туннель пропущен." -ForegroundColor Red
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Write-Host "SSH-туннель: forbidden-stars.ru (VPS:$vpsPort) → localhost:$localPort …" -ForegroundColor Green
|
||||||
|
$ssh = "ssh -N -R ${vpsPort}:localhost:${localPort} " +
|
||||||
|
"-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes " +
|
||||||
|
"${vpsUser}@${vpsHost}"
|
||||||
|
Start-Process powershell -ArgumentList @(
|
||||||
|
"-NoExit", "-Command",
|
||||||
|
"Write-Host 'SSH-туннель на forbidden-stars.ru. Закрой окно, чтобы остановить.' -ForegroundColor Cyan; $ssh"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Write-Host "APP_ENV = $appEnv" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
switch ($appEnv) {
|
switch ($appEnv) {
|
||||||
"development" {
|
"development" {
|
||||||
@@ -50,6 +79,10 @@ switch ($appEnv) {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " Бэк: http://127.0.0.1:8000 (Swagger: /api/docs)" -ForegroundColor Green
|
Write-Host " Бэк: http://127.0.0.1:8000 (Swagger: /api/docs)" -ForegroundColor Green
|
||||||
Write-Host " Фронт: http://127.0.0.1:5173" -ForegroundColor Green
|
Write-Host " Фронт: http://127.0.0.1:5173" -ForegroundColor Green
|
||||||
|
if ($localPublic -eq "vps") {
|
||||||
|
Start-VpsTunnel 5173
|
||||||
|
Write-Host " Публично: https://forbidden-stars.ru" -ForegroundColor Green
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"test" {
|
"test" {
|
||||||
$compose = Join-Path $root "docker-compose.test.yml"
|
$compose = Join-Path $root "docker-compose.test.yml"
|
||||||
@@ -60,6 +93,10 @@ switch ($appEnv) {
|
|||||||
Write-Host " Открыть: http://localhost:8080 (Swagger: /api/docs)" -ForegroundColor Green
|
Write-Host " Открыть: http://localhost:8080 (Swagger: /api/docs)" -ForegroundColor Green
|
||||||
Write-Host " Логи: docker compose -f docker-compose.test.yml logs -f"
|
Write-Host " Логи: docker compose -f docker-compose.test.yml logs -f"
|
||||||
Write-Host " Стоп: docker compose -f docker-compose.test.yml down -v"
|
Write-Host " Стоп: docker compose -f docker-compose.test.yml down -v"
|
||||||
|
if ($localPublic -eq "vps") {
|
||||||
|
Start-VpsTunnel 8080
|
||||||
|
Write-Host " Публично: https://forbidden-stars.ru" -ForegroundColor Green
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"production" {
|
"production" {
|
||||||
Write-Host "production лаунчером не запускается — прод обособлен." -ForegroundColor Yellow
|
Write-Host "production лаунчером не запускается — прод обособлен." -ForegroundColor Yellow
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
# test → docker compose прод-клон (порт 8080)
|
# test → docker compose прод-клон (порт 8080)
|
||||||
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
|
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
|
||||||
#
|
#
|
||||||
|
# LOCAL_PUBLIC=vps дополнительно поднимает SSH-туннель на VPS → https://forbidden-stars.ru
|
||||||
|
#
|
||||||
# Запуск: ./run.sh (при необходимости: chmod +x run.sh)
|
# Запуск: ./run.sh (при необходимости: chmod +x run.sh)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
root="$(cd "$(dirname "$0")" && pwd)"
|
root="$(cd "$(dirname "$0")" && pwd)"
|
||||||
@@ -13,11 +15,33 @@ root="$(cd "$(dirname "$0")" && pwd)"
|
|||||||
envf="$root/.env"
|
envf="$root/.env"
|
||||||
[ -f "$envf" ] || { echo "Нет .env в корне. Скопируйте: cp .env.example .env"; exit 1; }
|
[ -f "$envf" ] || { echo "Нет .env в корне. Скопируйте: cp .env.example .env"; exit 1; }
|
||||||
|
|
||||||
# APP_ENV из .env (последнее присвоение, без комментариев/пробелов).
|
# Достаём значение ключа из .env (последнее присвоение, без комментариев/пробелов).
|
||||||
app_env="$(grep -E '^[[:space:]]*APP_ENV[[:space:]]*=' "$envf" | tail -n1 \
|
env_val() {
|
||||||
| sed -E 's/^[^=]*=[[:space:]]*//; s/[[:space:]#].*$//' | tr 'A-Z' 'a-z')"
|
grep -E "^[[:space:]]*$1[[:space:]]*=" "$envf" | tail -n1 \
|
||||||
app_env="${app_env:-development}"
|
| sed -E 's/^[^=]*=[[:space:]]*//; s/[[:space:]#].*$//'
|
||||||
echo "APP_ENV = $app_env"
|
}
|
||||||
|
|
||||||
|
app_env="$(env_val APP_ENV | tr 'A-Z' 'a-z')"; app_env="${app_env:-development}"
|
||||||
|
local_public="$(env_val LOCAL_PUBLIC | tr 'A-Z' 'a-z')"; local_public="${local_public:-local}"
|
||||||
|
vps_host="$(env_val VPS_TUNNEL_HOST)"
|
||||||
|
vps_user="$(env_val VPS_TUNNEL_USER)"; vps_user="${vps_user:-tunnel}"
|
||||||
|
vps_port="$(env_val VPS_TUNNEL_PORT)"; vps_port="${vps_port:-9001}"
|
||||||
|
echo "APP_ENV = $app_env LOCAL_PUBLIC = $local_public"
|
||||||
|
|
||||||
|
# Поднимает SSH reverse-tunnel в фоне: VPS:$vps_port → localhost:$1 (если LOCAL_PUBLIC=vps).
|
||||||
|
start_tunnel() {
|
||||||
|
local local_port="$1"
|
||||||
|
if [ -z "$vps_host" ]; then
|
||||||
|
echo "LOCAL_PUBLIC=vps, но VPS_TUNNEL_HOST не задан — туннель пропущен."; return
|
||||||
|
fi
|
||||||
|
echo "SSH-туннель: forbidden-stars.ru (VPS:$vps_port) → localhost:$local_port …"
|
||||||
|
ssh -N -R "${vps_port}:localhost:${local_port}" \
|
||||||
|
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes \
|
||||||
|
"${vps_user}@${vps_host}" &
|
||||||
|
tunnel=$!
|
||||||
|
trap 'kill "$tunnel" 2>/dev/null || true' EXIT INT TERM
|
||||||
|
echo " Публично: https://forbidden-stars.ru"
|
||||||
|
}
|
||||||
|
|
||||||
case "$app_env" in
|
case "$app_env" in
|
||||||
development)
|
development)
|
||||||
@@ -25,12 +49,13 @@ case "$app_env" in
|
|||||||
[ -x "$py" ] || py="$root/backend/.venv/Scripts/python.exe" # Git Bash на Windows
|
[ -x "$py" ] || py="$root/backend/.venv/Scripts/python.exe" # Git Bash на Windows
|
||||||
[ -x "$py" ] || { echo "Нет venv. Сначала: cd backend && python -m venv .venv && pip install -e '.[dev]'"; exit 1; }
|
[ -x "$py" ] || { echo "Нет venv. Сначала: cd backend && python -m venv .venv && pip install -e '.[dev]'"; exit 1; }
|
||||||
[ -d "$root/frontend/node_modules" ] || { echo "Нет node_modules. Сначала: cd frontend && npm install"; exit 1; }
|
[ -d "$root/frontend/node_modules" ] || { echo "Нет node_modules. Сначала: cd frontend && npm install"; exit 1; }
|
||||||
echo "Запуск бэка (uvicorn --reload) и фронта (vite)… Ctrl+C — остановить оба."
|
echo "Запуск бэка (uvicorn --reload) и фронта (vite)… Ctrl+C — остановить всё."
|
||||||
( cd "$root/backend" && exec "$py" -m uvicorn app.main:app --reload ) &
|
( cd "$root/backend" && exec "$py" -m uvicorn app.main:app --reload ) &
|
||||||
back=$!
|
back=$!
|
||||||
trap 'kill "$back" 2>/dev/null || true' EXIT INT TERM
|
trap 'kill "$back" 2>/dev/null || true' EXIT INT TERM
|
||||||
echo " Бэк: http://127.0.0.1:8000 (Swagger: /api/docs)"
|
echo " Бэк: http://127.0.0.1:8000 (Swagger: /api/docs)"
|
||||||
echo " Фронт: http://127.0.0.1:5173"
|
echo " Фронт: http://127.0.0.1:5173"
|
||||||
|
[ "$local_public" = "vps" ] && start_tunnel 5173
|
||||||
( cd "$root/frontend" && npm run dev )
|
( cd "$root/frontend" && npm run dev )
|
||||||
;;
|
;;
|
||||||
test)
|
test)
|
||||||
@@ -39,6 +64,11 @@ case "$app_env" in
|
|||||||
echo " Открыть: http://localhost:8080 (Swagger: /api/docs)"
|
echo " Открыть: http://localhost:8080 (Swagger: /api/docs)"
|
||||||
echo " Логи: docker compose -f docker-compose.test.yml logs -f"
|
echo " Логи: docker compose -f docker-compose.test.yml logs -f"
|
||||||
echo " Стоп: docker compose -f docker-compose.test.yml down -v"
|
echo " Стоп: docker compose -f docker-compose.test.yml down -v"
|
||||||
|
if [ "$local_public" = "vps" ]; then
|
||||||
|
start_tunnel 8080
|
||||||
|
echo " (туннель в фоне; Ctrl+C — закрыть туннель. Контейнер останавливается отдельно.)"
|
||||||
|
wait "$tunnel" 2>/dev/null || true
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
production)
|
production)
|
||||||
echo "production лаунчером не запускается — прод обособлен."
|
echo "production лаунчером не запускается — прод обособлен."
|
||||||
|
|||||||
Reference in New Issue
Block a user