v0.2 - Публикация через домен, настройка ssh-туннеля к VPS
This commit is contained in:
+23
-9
@@ -1,8 +1,8 @@
|
||||
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
||||
# ║ Forbidden Stars — единый .env (dev и prod) ║
|
||||
# ║ Скопируйте в `.env`, заполните секреты. Реальный `.env` в git НЕ идёт. ║
|
||||
# ║ Переключение dev/prod — одной строкой APP_ENV (ниже). ║
|
||||
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Forbidden Stars — единый .env (dev / test / prod)
|
||||
# Скопируйте в .env, заполните секреты. Реальный .env в git НЕ идёт.
|
||||
# Окружение — строкой APP_ENV (ниже); публикация локалки наружу — LOCAL_PUBLIC.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# ─── ГЛАВНЫЙ ПЕРЕКЛЮЧАТЕЛЬ ────────────────────────────────────────────────────
|
||||
# Этот параметр читает ЛАУНЧЕР (run.ps1 / run.sh) и решает, что запускать:
|
||||
@@ -12,6 +12,18 @@
|
||||
# Прод-контейнер ИГНОРИРУЕТ это значение и всегда production.
|
||||
APP_ENV=development
|
||||
|
||||
# ─── ПУБЛИКАЦИЯ ЧЕРЕЗ ДОМЕН (VPS-туннель) ─────────────────────────────────────
|
||||
# LOCAL_PUBLIC — только для DEV на твоём ПК: local = приложение лишь на localhost;
|
||||
# vps = лаунчер (run.ps1) дополнительно поднимает SSH-туннель → дев на forbidden-stars.ru.
|
||||
# TEST и PROD выставляют себя сами через туннель-КОНТЕЙНЕР (docker-compose*.yml) — им
|
||||
# LOCAL_PUBLIC не нужен, но VPS_TUNNEL_HOST/USER ниже они тоже читают.
|
||||
LOCAL_PUBLIC=local
|
||||
# Параметры VPS: их читают и dev-туннель (run.ps1), и туннель-контейнер test/prod.
|
||||
# Ключ туннеля — в deploy/tunnel/id_tunnel (в git не идёт); pubkey → authorized_keys у tunnel@VPS.
|
||||
VPS_TUNNEL_HOST=186.246.51.17
|
||||
VPS_TUNNEL_USER=tunnel
|
||||
VPS_TUNNEL_PORT=9001 # слот VPS для dev-туннеля (forbidden-stars.ru); контейнеры: prod=9000, test=9001
|
||||
|
||||
# ─── АДМИНИСТРАТОР (вход по логину/паролю, отдельно от Telegram) ──────────────
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=change-me-admin-password
|
||||
@@ -19,10 +31,12 @@ ADMIN_BOOTSTRAP_ENABLED=true
|
||||
|
||||
# ─── АУТЕНТИФИКАЦИЯ ИГРОКОВ ───────────────────────────────────────────────────
|
||||
# Методы входа задаёт APP_ENV: dev → Telegram + stub (вход по нику), prod → только
|
||||
# Telegram. Отдельного переключателя нет. Для Telegram нужны токен и юзернейм бота
|
||||
# (@BotFather) и /setdomain на ваш HTTPS-домен.
|
||||
# Telegram. Для Telegram нужны токен и юзернейм бота (@BotFather). /setdomain у
|
||||
# BotFather укажи на ОБА домена, где открывается виджет: forbiddenstars.ru (prod)
|
||||
# и forbidden-stars.ru (dev/test).
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_BOT_USERNAME=
|
||||
# Внешний адрес (зарезервировано): prod https://forbiddenstars.ru; dev/test https://forbidden-stars.ru.
|
||||
PUBLIC_BASE_URL=
|
||||
|
||||
# ─── БЕЗОПАСНОСТЬ / СЕССИИ ────────────────────────────────────────────────────
|
||||
@@ -33,8 +47,8 @@ SECRET_KEY=change-me-dev-secret-not-for-production
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_USER_TTL_MINUTES=10080
|
||||
JWT_ADMIN_TTL_MINUTES=480
|
||||
# dev/test (локально, по HTTP) → false; production за HTTPS на Pi → true.
|
||||
COOKIE_SECURE=false
|
||||
# COOKIE_SECURE задаётся АВТОМАТИЧЕСКИ по окружению (HTTPS-домен ⇒ Secure-cookie):
|
||||
# dev+localhost → false; dev через VPS, test, prod → true. Вручную задавать НЕ нужно.
|
||||
COOKIE_DOMAIN=
|
||||
|
||||
# ─── БАЗА ДАННЫХ (структура общая, файлы РАЗНЫЕ; выбор по APP_ENV) ────────────
|
||||
|
||||
+10
@@ -19,6 +19,12 @@ backend/dev.db*
|
||||
.env
|
||||
!.env.example
|
||||
|
||||
# Сертификаты и приватные ключи (живут на VPS/Pi/ПК, не в репозитории)
|
||||
*.pem
|
||||
*.key
|
||||
id_tunnel*
|
||||
deploy/**/certs/
|
||||
|
||||
# Node / фронт
|
||||
node_modules/
|
||||
frontend/dist/
|
||||
@@ -31,6 +37,10 @@ backend/openapi.json
|
||||
# Бэкапы (создаёт scripts/backup.sh на Pi)
|
||||
backups/
|
||||
|
||||
# AI-ассистенты (локальные, в репозиторий не идут)
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
|
||||
# Редактор / ОС
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
@@ -144,9 +144,9 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
||||
|
||||
**Настройка Telegram (когда будете подключать реальный вход):**
|
||||
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=...` (без `@`).
|
||||
4. Домену нужен HTTPS (например, Cloudflare Tunnel) — виджет не работает по голому HTTP.
|
||||
4. Виджету нужен HTTPS-домен (см. «Домен и публикация») — по голому HTTP/localhost он не работает.
|
||||
|
||||
Админ-вход (секретная панель, логин+пароль) — отдельный механизм, доступен во всех окружениях.
|
||||
|
||||
@@ -184,6 +184,26 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
||||
|
||||
Секреты (`.env`) и данные (`data/`, `*.db`) в git не идут — см. `.gitignore`.
|
||||
|
||||
## Домен и публикация
|
||||
|
||||
Публичные адреса отдаёт **VPS-привратник** (Caddy + HTTPS твоими сертификатами), а
|
||||
приложение само открывает к нему SSH reverse-туннель (дома белого IP нет — CGNAT).
|
||||
У **test/prod** туннель — **отдельный контейнер** в их `docker-compose`, и портов на хост
|
||||
они не публикуют (доступны только через домен):
|
||||
|
||||
| | домен | как выставляется | слот VPS |
|
||||
|---|---|---|---|
|
||||
| prod | `forbiddenstars.ru` | туннель-контейнер (постоянно) → `app:8000` | 9000 |
|
||||
| test | `forbidden-stars.ru` | туннель-контейнер → `app:8000` | 9001 |
|
||||
| dev | `forbidden-stars.ru` | `run.ps1` при `LOCAL_PUBLIC=vps` → `localhost:5173` | 9001 |
|
||||
|
||||
- Прод (9000) и dev/test (9001) на **разных слотах/доменах** → прод и (dev|test) работают
|
||||
одновременно. Dev и test делят слот 9001 → по очереди.
|
||||
- Dev по умолчанию только на localhost; `LOCAL_PUBLIC=vps` + `run.ps1` выставляет его на домен.
|
||||
- `COOKIE_SECURE` выводится автоматически (HTTPS-домен ⇒ Secure-cookie; dev+localhost ⇒ нет).
|
||||
- Пошаговая настройка — в [`deploy/`](deploy/README.md): `vps/` (Caddy, сертификаты, юзер `tunnel`),
|
||||
`pi/` (app + туннель в Docker), образ туннеля — `deploy/tunnel/`. Ключ — `deploy/tunnel/id_tunnel`.
|
||||
|
||||
## Дополнения и фракции
|
||||
|
||||
| Дополнение | Фракции |
|
||||
|
||||
@@ -27,6 +27,11 @@ class Settings(BaseSettings):
|
||||
app_env: str = "development"
|
||||
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).
|
||||
# Хранение всегда в UTC; смещение применяется к «дате игры» и отображению.
|
||||
app_tz_offset_hours: int = 3
|
||||
@@ -45,7 +50,7 @@ class Settings(BaseSettings):
|
||||
jwt_algorithm: str = "HS256"
|
||||
jwt_user_ttl_minutes: int = 60 * 24 * 7
|
||||
jwt_admin_ttl_minutes: int = 60 * 8
|
||||
cookie_secure: bool = False
|
||||
# cookie_secure НЕ задаётся вручную — выводится из окружения (см. property ниже).
|
||||
cookie_domain: str | None = None
|
||||
|
||||
# Аутентификация. Методы входа определяются окружением (dev: telegram+stub,
|
||||
@@ -92,6 +97,12 @@ class Settings(BaseSettings):
|
||||
def upload_dir(self) -> str:
|
||||
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
|
||||
def cookie_domain_value(self) -> str | None:
|
||||
return self.cookie_domain or None
|
||||
|
||||
+3
-1
@@ -77,7 +77,9 @@ def create_app() -> FastAPI:
|
||||
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(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins_list,
|
||||
|
||||
@@ -8,4 +8,7 @@ echo "[entrypoint] Бутстрап справочников и админист
|
||||
python -m app.bootstrap
|
||||
|
||||
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="*"
|
||||
|
||||
@@ -8,6 +8,7 @@ import os
|
||||
# обращение к app.db.session.engine не затронет файлы дева/прода, какой бы
|
||||
# APP_ENV ни был унаследован из окружения.
|
||||
os.environ["APP_ENV"] = "development"
|
||||
os.environ["LOCAL_PUBLIC"] = "local" # cookie_secure=False детерминированно (не зависит от .env разработчика)
|
||||
os.environ["DEV_DATABASE_URL"] = "sqlite://"
|
||||
os.environ["FS_STARTUP_BOOTSTRAP"] = "0" # тесты сами поднимают схему/данные
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Публикация: домены, 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`).
|
||||
@@ -0,0 +1,46 @@
|
||||
# Raspberry Pi — ПРОД (`forbiddenstars.ru`)
|
||||
|
||||
Pi запускает приложение **и туннель** в Docker: `docker compose up` поднимает два сервиса —
|
||||
`app` (FastAPI + SPA) и `tunnel` (ssh-контейнер). У `app` **портов на хост нет**, наружу он
|
||||
доступен только через туннель-контейнер (`ssh -R 9000:app:8000` к VPS). Публичная точка —
|
||||
VPS, домен `forbiddenstars.ru`. Снаружи Pi недоступен (CGNAT) — туннель сам стучится наружу.
|
||||
|
||||
## 1. Ключ для туннеля
|
||||
Туннель-контейнер ходит на `tunnel@VPS` по ключу из `deploy/tunnel/id_tunnel` (gitignored).
|
||||
Создай ключ и добавь его pubkey в `authorized_keys` пользователя `tunnel` на VPS:
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f deploy/tunnel/id_tunnel -N ""
|
||||
ssh-copy-id -i deploy/tunnel/id_tunnel.pub tunnel@186.246.51.17
|
||||
# (или вручную: содержимое deploy/tunnel/id_tunnel.pub -> /home/tunnel/.ssh/authorized_keys на VPS)
|
||||
```
|
||||
|
||||
## 2. .env
|
||||
```bash
|
||||
cp .env.example .env # заполни секреты; APP_ENV контейнер форсит в production
|
||||
```
|
||||
Проверь, что заданы параметры VPS (их читает сервис `tunnel`):
|
||||
```
|
||||
VPS_TUNNEL_HOST=186.246.51.17
|
||||
VPS_TUNNEL_USER=tunnel
|
||||
```
|
||||
|
||||
## 3. Запуск
|
||||
```bash
|
||||
docker compose up -d --build # поднимет app + tunnel
|
||||
docker compose ps # оба up; tunnel стартует после healthy app
|
||||
docker compose logs -f tunnel # ждём строку: [tunnel] -R 9000:app:8000 -> tunnel@...
|
||||
```
|
||||
Открой `https://forbiddenstars.ru` — должен отдать прод.
|
||||
|
||||
## Обновление
|
||||
```bash
|
||||
git pull
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## Бэкап БД
|
||||
`scripts/backup.sh` (консистентная онлайн-копия SQLite; по cron на хосте). Том БД — `db-data`.
|
||||
|
||||
Логи: `docker compose logs -f app` / `... tunnel`. Если `https://forbiddenstars.ru` отдаёт
|
||||
502 — значит туннель не поднят: смотри `docker compose logs tunnel` (чаще всего ключ не в
|
||||
`authorized_keys` на VPS или не заданы `VPS_TUNNEL_*` в `.env`).
|
||||
@@ -0,0 +1,2 @@
|
||||
# Приватный ключ монтируется в рантайме, в образ его НЕ кладём.
|
||||
id_tunnel*
|
||||
@@ -0,0 +1,8 @@
|
||||
# Крошечный контейнер SSH reverse-туннеля к VPS.
|
||||
# Держит -R <VPS_TUNNEL_PORT>:app:8000 на VPS, отдавая приложение по сети compose.
|
||||
# Переподключение обеспечивает Docker (restart: unless-stopped) при выходе ssh.
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache openssh-client
|
||||
COPY tunnel.sh /usr/local/bin/tunnel.sh
|
||||
RUN sed -i 's/\r$//' /usr/local/bin/tunnel.sh && chmod +x /usr/local/bin/tunnel.sh
|
||||
ENTRYPOINT ["/usr/local/bin/tunnel.sh"]
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
# SSH reverse-туннель из контейнера на VPS:
|
||||
# VPS:127.0.0.1:$VPS_TUNNEL_PORT -> $UPSTREAM (сервис app по сети compose)
|
||||
# Ключ монтируется снаружи в /key/id_tunnel (см. docker-compose*.yml).
|
||||
set -eu
|
||||
|
||||
: "${VPS_TUNNEL_HOST:?VPS_TUNNEL_HOST is required}"
|
||||
VPS_TUNNEL_USER="${VPS_TUNNEL_USER:-tunnel}"
|
||||
: "${VPS_TUNNEL_PORT:?VPS_TUNNEL_PORT is required}"
|
||||
UPSTREAM="${UPSTREAM:-app:8000}"
|
||||
|
||||
# Ключ должен быть смонтирован файлом. Если тут пусто/папка — Docker создал каталог,
|
||||
# потому что файла deploy/tunnel/id_tunnel на хосте нет.
|
||||
if [ ! -f /key/id_tunnel ]; then
|
||||
echo "[tunnel] ERROR: /key/id_tunnel is not a regular file."
|
||||
echo "[tunnel] Put your private key at deploy/tunnel/id_tunnel on the host (its pubkey must be in authorized_keys of tunnel@VPS)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Копируем ключ и выставляем строгие права (с хоста права могут не годиться, особенно с Windows).
|
||||
mkdir -p /root/.ssh
|
||||
cp /key/id_tunnel /root/.ssh/id_tunnel
|
||||
chmod 600 /root/.ssh/id_tunnel
|
||||
|
||||
echo "[tunnel] -R ${VPS_TUNNEL_PORT}:${UPSTREAM} -> ${VPS_TUNNEL_USER}@${VPS_TUNNEL_HOST}"
|
||||
exec ssh -N \
|
||||
-o BatchMode=yes \
|
||||
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
|
||||
-o ExitOnForwardFailure=yes \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o UserKnownHostsFile=/root/.ssh/known_hosts \
|
||||
-i /root/.ssh/id_tunnel \
|
||||
-R "${VPS_TUNNEL_PORT}:${UPSTREAM}" \
|
||||
"${VPS_TUNNEL_USER}@${VPS_TUNNEL_HOST}"
|
||||
@@ -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,100 @@
|
||||
# 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. Сертификаты
|
||||
|
||||
Caddy читает **PEM** (текст с `-----BEGIN CERTIFICATE-----`). Расширение неважно — `.crt`
|
||||
от GlobalSign обычно УЖЕ PEM. Проверь первой строкой: `head -1 файл.crt`.
|
||||
- если `-----BEGIN CERTIFICATE-----` → это PEM, конвертировать НЕ нужно;
|
||||
- если бинарь/абракадабра → это DER, сконвертируй: `openssl x509 -inform DER -in файл.crt -out файл.pem`.
|
||||
|
||||
На каждый домен нужны два файла:
|
||||
- `fullchain.pem` = твой сертификат (leaf) **+** промежуточный(е) GlobalSign, в одном файле,
|
||||
именно в таком порядке (сначала leaf, потом промежуточный).
|
||||
- `privkey.pem` = приватный ключ (`-----BEGIN PRIVATE KEY-----`, без пароля; обычно это `.key`).
|
||||
|
||||
Удобно собрать прямо на VPS — залей свои файлы и склей:
|
||||
```bash
|
||||
mkdir -p /etc/caddy/certs/forbidden-stars.ru /root/certs-tmp
|
||||
# с локальной машины (пример для домена forbidden-stars.ru):
|
||||
scp forbidden-stars.crt intermediate.crt forbidden-stars.key root@186.246.51.17:/root/certs-tmp/
|
||||
# на VPS — собрать fullchain (leaf + промежуточный) и положить ключ:
|
||||
cat /root/certs-tmp/www_forbidden_stars_ru_2026_12_31.crt /root/certs-tmp/intermediate_pem_globalsign_ssl_dv_free_1.crt \
|
||||
> /etc/caddy/certs/forbidden-stars.ru/fullchain.pem
|
||||
cp /root/certs-tmp/forbidden-stars.key /etc/caddy/certs/forbidden-stars.ru/privkey.pem
|
||||
# то же для forbiddenstars.ru (свои crt/intermediate/key), затем права
|
||||
# (папкам нужен x-бит → 750, файлам 640; иначе пользователь caddy не прочитает):
|
||||
chown -R caddy:caddy /etc/caddy/certs
|
||||
find /etc/caddy/certs -type d -exec chmod 750 {} \;
|
||||
find /etc/caddy/certs -type f -exec chmod 640 {} \;
|
||||
rm -rf /root/certs-tmp
|
||||
```
|
||||
> Если GlobalSign дал не отдельный промежуточный, а готовый bundle/chain — используй его
|
||||
> как «промежуточную» часть. Если дал один файл, где leaf и chain уже вместе, — это и есть
|
||||
> `fullchain.pem` (склейка не нужна).
|
||||
|
||||
## 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`.
|
||||
+27
-8
@@ -1,11 +1,10 @@
|
||||
# Локальный «клон прода» в контейнере — для проверки прод-сборки на своей машине
|
||||
# (а не на Raspberry Pi). Тот же образ, что и прод, но изолированные тома, единый .env
|
||||
# и порт 8080 (чтобы не конфликтовать с dev-uvicorn на :8000).
|
||||
# (а не на Raspberry Pi). Тот же образ, что и прод, изолированные тома, единый .env.
|
||||
# Портов на хост НЕТ: тест виден ТОЛЬКО снаружи на https://forbidden-stars.ru через
|
||||
# сервис tunnel (контейнер ssh -R 9001:app:8000 на VPS).
|
||||
#
|
||||
# Обычно запускается лаунчером при APP_ENV=test (run.ps1 / run.sh). Вручную:
|
||||
# docker compose -f docker-compose.test.yml up --build -d
|
||||
# Открыть: http://localhost:8080 (Swagger: /api/docs)
|
||||
# Остановить и стереть данные: docker compose -f docker-compose.test.yml down -v
|
||||
# Запуск (обычно лаунчером при APP_ENV=test): docker compose -f docker-compose.test.yml up --build -d
|
||||
# Остановить и стереть данные: docker compose -f docker-compose.test.yml down -v
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
@@ -19,8 +18,8 @@ services:
|
||||
# Окружение test: прод-клон, но отличимый от прода (см. config.is_test).
|
||||
# Форсим здесь, чтобы значение не зависело от APP_ENV внутри .env.
|
||||
APP_ENV: test
|
||||
ports:
|
||||
- "8080:8000"
|
||||
# Портов на хост НЕТ: тест доступен только изнутри сети compose; наружу — через
|
||||
# сервис tunnel (ниже) на forbidden-stars.ru. Из LAN/localhost — недоступно.
|
||||
volumes:
|
||||
- db-data-test:/data # изолированные тестовые данные
|
||||
- uploads-data-test:/data/uploads
|
||||
@@ -36,6 +35,26 @@ services:
|
||||
start_period: 40s
|
||||
mem_limit: 512m
|
||||
|
||||
# SSH reverse-туннель к VPS: forbidden-stars.ru (VPS:9001) -> app:8000 (по сети compose).
|
||||
# Ключ — ./deploy/tunnel/id_tunnel (gitignore), pubkey в authorized_keys у tunnel@VPS.
|
||||
# Внимание: слот 9001 общий с dev-туннелем (run.ps1); поднимай что-то одно за раз.
|
||||
tunnel:
|
||||
build: ./deploy/tunnel
|
||||
image: forbidden-stars-tunnel:test
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
VPS_TUNNEL_HOST: ${VPS_TUNNEL_HOST}
|
||||
VPS_TUNNEL_USER: ${VPS_TUNNEL_USER:-tunnel}
|
||||
VPS_TUNNEL_PORT: "9001" # forbidden-stars.ru (dev/test)
|
||||
UPSTREAM: app:8000
|
||||
volumes:
|
||||
- ./deploy/tunnel/id_tunnel:/key/id_tunnel:ro
|
||||
mem_limit: 64m
|
||||
|
||||
volumes:
|
||||
db-data-test:
|
||||
uploads-data-test:
|
||||
|
||||
+22
-2
@@ -10,8 +10,8 @@ services:
|
||||
# Прод обособлен: контейнер ВСЕГДА production и ИГНОРИРУЕТ APP_ENV из .env
|
||||
# (хоть development, хоть test). БД=/data, STATIC_DIR=/app/static — из Dockerfile.
|
||||
APP_ENV: production
|
||||
ports:
|
||||
- "8000:8000"
|
||||
# Портов на хост НЕТ: приложение доступно только изнутри сети compose.
|
||||
# Наружу его выставляет сервис tunnel (ниже) через VPS. Из LAN/localhost — недоступно.
|
||||
volumes:
|
||||
- db-data:/data # БД SQLite + WAL-сайдкары
|
||||
- uploads-data:/data/uploads # задел под фото/видео
|
||||
@@ -33,6 +33,26 @@ services:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
|
||||
# SSH reverse-туннель к VPS: forbiddenstars.ru (VPS:9000) -> app:8000 (по сети compose).
|
||||
# Ключ — в ./deploy/tunnel/id_tunnel (gitignore), его pubkey в authorized_keys у tunnel@VPS.
|
||||
# VPS_TUNNEL_HOST/USER берутся из .env (на Pi заполни их там же).
|
||||
tunnel:
|
||||
build: ./deploy/tunnel
|
||||
image: forbidden-stars-tunnel:latest
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
VPS_TUNNEL_HOST: ${VPS_TUNNEL_HOST}
|
||||
VPS_TUNNEL_USER: ${VPS_TUNNEL_USER:-tunnel}
|
||||
VPS_TUNNEL_PORT: "9000" # forbiddenstars.ru (прод); у test — 9001
|
||||
UPSTREAM: app:8000
|
||||
volumes:
|
||||
- ./deploy/tunnel/id_tunnel:/key/id_tunnel:ro
|
||||
mem_limit: 64m
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
uploads-data:
|
||||
|
||||
@@ -11,6 +11,10 @@ export default defineConfig({
|
||||
// доступен и из локальной сети (для разработки на своей машине это нормально).
|
||||
host: true,
|
||||
port: 5173,
|
||||
// Разрешаем хост домена для режима LOCAL_PUBLIC=vps (dev виден на forbidden-stars.ru
|
||||
// через SSH-туннель). localhost/127.0.0.1 vite пропускает по умолчанию.
|
||||
// HMR через домен может не подключиться — это ок, само приложение работает.
|
||||
allowedHosts: ["forbidden-stars.ru"],
|
||||
proxy: {
|
||||
"/api": {
|
||||
// Явный IPv4: на Windows "localhost" может резолвиться в IPv6 ::1,
|
||||
|
||||
@@ -1,28 +1,72 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
||||
# ║ Единый лаунчер dev / test. Что запускать — решает APP_ENV из корневого .env ║
|
||||
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
||||
# development → uvicorn --reload (бэк) + vite (фронт), нативно, в двух окнах
|
||||
# test → docker compose прод-клон (порт 8080)
|
||||
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
|
||||
# Unified launcher for dev / test. APP_ENV in the root .env decides what to run.
|
||||
# development -> uvicorn --reload (backend) + vite (frontend), native, two windows
|
||||
# test -> docker compose prod-clone (port 8080)
|
||||
# production -> NOT started by launcher (prod is separate: docker compose up -d on Pi)
|
||||
#
|
||||
# Запуск: .\run.ps1
|
||||
# Если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
||||
# LOCAL_PUBLIC=vps (dev only) opens an SSH tunnel to the VPS, exposing dev at
|
||||
# https://forbidden-stars.ru. test/prod expose themselves via an in-container tunnel.
|
||||
#
|
||||
# Run: .\run.ps1
|
||||
# If blocked by policy: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
||||
#
|
||||
# NOTE: keep this file ASCII-only. Windows PowerShell 5.1 reads BOM-less .ps1 in the
|
||||
# system ANSI codepage, so non-ASCII (Cyrillic / box chars) breaks the parser.
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = $PSScriptRoot
|
||||
|
||||
$envFile = Join-Path $root ".env"
|
||||
if (-not (Test-Path $envFile)) {
|
||||
Write-Host "Нет .env в корне. Скопируйте: Copy-Item .env.example .env" -ForegroundColor Red
|
||||
Write-Host "No .env in repo root. Create it: Copy-Item .env.example .env" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Читаем APP_ENV из .env (последнее присвоение побеждает).
|
||||
$appEnv = "development"
|
||||
foreach ($line in Get-Content $envFile) {
|
||||
if ($line -match '^\s*APP_ENV\s*=\s*([^#\s]+)') { $appEnv = $matches[1].Trim().ToLower() }
|
||||
# Read a key from .env (last assignment wins; comments ignored).
|
||||
function Get-EnvVal([string]$name, [string]$default) {
|
||||
$val = $default
|
||||
foreach ($line in Get-Content $envFile) {
|
||||
if ($line -match "^\s*$name\s*=\s*([^#\s]+)") { $val = $matches[1].Trim() }
|
||||
}
|
||||
return $val
|
||||
}
|
||||
Write-Host "APP_ENV = $appEnv" -ForegroundColor Cyan
|
||||
|
||||
$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
|
||||
|
||||
# Open SSH reverse tunnel VPS:$vpsPort -> localhost:$localPort (in a separate window).
|
||||
function Start-VpsTunnel([int]$localPort) {
|
||||
if (-not $vpsHost) {
|
||||
Write-Host "LOCAL_PUBLIC=vps but VPS_TUNNEL_HOST is empty in .env - tunnel skipped." -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
Write-Host "SSH tunnel: 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 tunnel to forbidden-stars.ru. Close this window to stop.' -ForegroundColor Cyan; $ssh"
|
||||
)
|
||||
}
|
||||
|
||||
# Kill any leftover tunnel from a previous run, so LOCAL_PUBLIC is authoritative each launch
|
||||
# (the tunnel lives in its own window and would otherwise keep the domain served).
|
||||
function Stop-VpsTunnel {
|
||||
Get-CimInstance Win32_Process -Filter "Name='ssh.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.CommandLine -match "-R\s+${vpsPort}:localhost" } |
|
||||
ForEach-Object {
|
||||
Write-Host "Closing leftover tunnel (pid $($_.ProcessId))..." -ForegroundColor DarkYellow
|
||||
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Start from a clean tunnel state; a fresh tunnel is opened only if LOCAL_PUBLIC=vps below.
|
||||
Stop-VpsTunnel
|
||||
|
||||
switch ($appEnv) {
|
||||
"development" {
|
||||
@@ -30,15 +74,15 @@ switch ($appEnv) {
|
||||
$frontend = Join-Path $root "frontend"
|
||||
$py = Join-Path $backend ".venv\Scripts\python.exe"
|
||||
if (-not (Test-Path $py)) {
|
||||
Write-Host "Нет venv: $py" -ForegroundColor Red
|
||||
Write-Host "Сначала: cd backend; python -m venv .venv; .\.venv\Scripts\Activate.ps1; pip install -e `".[dev]`"" -ForegroundColor Yellow
|
||||
Write-Host "venv not found: $py" -ForegroundColor Red
|
||||
Write-Host "First: cd backend; python -m venv .venv; .\.venv\Scripts\Activate.ps1; pip install -e `".[dev]`"" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
if (-not (Test-Path (Join-Path $frontend "node_modules"))) {
|
||||
Write-Host "Нет node_modules. Сначала: cd frontend; npm install" -ForegroundColor Red
|
||||
Write-Host "node_modules not found. First: cd frontend; npm install" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Запуск бэка (uvicorn --reload) и фронта (vite) в отдельных окнах…" -ForegroundColor Green
|
||||
Write-Host "Starting backend (uvicorn --reload) and frontend (vite) in separate windows..." -ForegroundColor Green
|
||||
Start-Process powershell -ArgumentList @(
|
||||
"-NoExit", "-Command",
|
||||
"Set-Location '$backend'; & '$py' -m uvicorn app.main:app --reload"
|
||||
@@ -48,26 +92,47 @@ switch ($appEnv) {
|
||||
"Set-Location '$frontend'; npm run dev"
|
||||
)
|
||||
Write-Host ""
|
||||
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 " Backend: http://127.0.0.1:8000 (Swagger: /api/docs)" -ForegroundColor Green
|
||||
Write-Host " Frontend: http://127.0.0.1:5173" -ForegroundColor Green
|
||||
if ($localPublic -eq "vps") {
|
||||
Start-VpsTunnel 5173
|
||||
Write-Host " Public: https://forbidden-stars.ru" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
"test" {
|
||||
$compose = Join-Path $root "docker-compose.test.yml"
|
||||
Write-Host "Сборка и запуск прод-клона в Docker (порт 8080)…" -ForegroundColor Green
|
||||
docker info --format '{{.ServerVersion}}' *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Docker daemon is not reachable. Start Docker Desktop, wait until it is running, then re-run .\run.ps1" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
$keyFile = Join-Path $root "deploy\tunnel\id_tunnel"
|
||||
if (-not (Test-Path $keyFile -PathType Leaf)) {
|
||||
if (Test-Path $keyFile -PathType Container) {
|
||||
Write-Host "deploy\tunnel\id_tunnel is a DIRECTORY - Docker auto-created it because the key file was missing." -ForegroundColor Red
|
||||
Write-Host "Remove it first: Remove-Item -Recurse -Force deploy\tunnel\id_tunnel" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "No SSH key at deploy\tunnel\id_tunnel (the tunnel container needs it)." -ForegroundColor Red
|
||||
}
|
||||
Write-Host "Add your VPS-authorized key: Copy-Item `$env:USERPROFILE\.ssh\id_ed25519 deploy\tunnel\id_tunnel" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Building and starting prod-clone in Docker (app + in-container tunnel)..." -ForegroundColor Green
|
||||
docker compose -f $compose up --build -d
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
Write-Host ""
|
||||
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 down -v"
|
||||
Write-Host " Public: https://forbidden-stars.ru (Swagger: /api/docs)" -ForegroundColor Green
|
||||
Write-Host " (no host port - reachable only via the domain; tunnel runs inside compose)"
|
||||
Write-Host " Logs: docker compose -f docker-compose.test.yml logs -f"
|
||||
Write-Host " Stop: docker compose -f docker-compose.test.yml down -v"
|
||||
}
|
||||
"production" {
|
||||
Write-Host "production лаунчером не запускается — прод обособлен." -ForegroundColor Yellow
|
||||
Write-Host "Деплой на Pi (из ветки main): docker compose up -d --build"
|
||||
Write-Host "production is not started by the launcher - prod is separate." -ForegroundColor Yellow
|
||||
Write-Host "Deploy on Pi (from main branch): docker compose up -d --build"
|
||||
exit 1
|
||||
}
|
||||
default {
|
||||
Write-Host "Неизвестный APP_ENV='$appEnv'. Допустимо: development | test | production." -ForegroundColor Red
|
||||
Write-Host "Unknown APP_ENV='$appEnv'. Allowed: development | test | production." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
# test → docker compose прод-клон (порт 8080)
|
||||
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
|
||||
#
|
||||
# LOCAL_PUBLIC=vps (только dev) поднимает SSH-туннель на VPS → https://forbidden-stars.ru
|
||||
# test/prod выставляют себя сами через туннель-контейнер (см. docker-compose*.yml).
|
||||
#
|
||||
# Запуск: ./run.sh (при необходимости: chmod +x run.sh)
|
||||
set -euo pipefail
|
||||
root="$(cd "$(dirname "$0")" && pwd)"
|
||||
@@ -13,11 +16,33 @@ root="$(cd "$(dirname "$0")" && pwd)"
|
||||
envf="$root/.env"
|
||||
[ -f "$envf" ] || { echo "Нет .env в корне. Скопируйте: cp .env.example .env"; exit 1; }
|
||||
|
||||
# APP_ENV из .env (последнее присвоение, без комментариев/пробелов).
|
||||
app_env="$(grep -E '^[[:space:]]*APP_ENV[[:space:]]*=' "$envf" | tail -n1 \
|
||||
| sed -E 's/^[^=]*=[[:space:]]*//; s/[[:space:]#].*$//' | tr 'A-Z' 'a-z')"
|
||||
app_env="${app_env:-development}"
|
||||
echo "APP_ENV = $app_env"
|
||||
# Достаём значение ключа из .env (последнее присвоение, без комментариев/пробелов).
|
||||
env_val() {
|
||||
grep -E "^[[:space:]]*$1[[:space:]]*=" "$envf" | tail -n1 \
|
||||
| sed -E 's/^[^=]*=[[:space:]]*//; s/[[:space:]#].*$//'
|
||||
}
|
||||
|
||||
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
|
||||
development)
|
||||
@@ -25,20 +50,23 @@ case "$app_env" in
|
||||
[ -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; }
|
||||
[ -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 ) &
|
||||
back=$!
|
||||
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:5173"
|
||||
[ "$local_public" = "vps" ] && start_tunnel 5173
|
||||
( cd "$root/frontend" && npm run dev )
|
||||
;;
|
||||
test)
|
||||
echo "Сборка и запуск прод-клона в Docker (порт 8080)…"
|
||||
docker info >/dev/null 2>&1 || { echo "Docker-демон недоступен — запусти Docker и повтори ./run.sh"; exit 1; }
|
||||
echo "Сборка и запуск прод-клона в Docker (app + туннель в контейнере)…"
|
||||
docker compose -f "$root/docker-compose.test.yml" up --build -d
|
||||
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 down -v"
|
||||
echo " Публично: https://forbidden-stars.ru (Swagger: /api/docs)"
|
||||
echo " (портов на хост нет — только через домен; туннель живёт внутри compose)"
|
||||
echo " Логи: docker compose -f docker-compose.test.yml logs -f"
|
||||
echo " Стоп: docker compose -f docker-compose.test.yml down -v"
|
||||
;;
|
||||
production)
|
||||
echo "production лаунчером не запускается — прод обособлен."
|
||||
|
||||
Reference in New Issue
Block a user