From b04fbb2e171a7fe3376f3633dfee9e29e4e4bb90 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Tue, 16 Jun 2026 19:15:54 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD=D0=B0=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=B5=D0=B2=D0=B0,=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B4=D0=B0.=20=D0=A1?= =?UTF-8?q?=D0=BE=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20ssh-=D1=82=D1=83=D0=BD=D0=BD=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 30 ++++++--- .gitignore | 6 ++ README.md | 22 +++++- backend/app/core/config.py | 13 +++- backend/app/main.py | 4 +- backend/entrypoint.sh | 5 +- deploy/README.md | 35 ++++++++++ deploy/pi/README.md | 38 +++++++++++ deploy/pi/forbidden-stars-tunnel.service | 27 ++++++++ deploy/vps/Caddyfile | 22 ++++++ deploy/vps/README.md | 85 ++++++++++++++++++++++++ frontend/vite.config.ts | 4 ++ run.ps1 | 49 ++++++++++++-- run.sh | 42 ++++++++++-- 14 files changed, 356 insertions(+), 26 deletions(-) create mode 100644 deploy/README.md create mode 100644 deploy/pi/README.md create mode 100644 deploy/pi/forbidden-stars-tunnel.service create mode 100644 deploy/vps/Caddyfile create mode 100644 deploy/vps/README.md diff --git a/.env.example b/.env.example index 8225661..3fe6022 100644 --- a/.env.example +++ b/.env.example @@ -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,16 @@ # Прод-контейнер ИГНОРИРУЕТ это значение и всегда production. 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) ────────────── ADMIN_USERNAME=admin ADMIN_PASSWORD=change-me-admin-password @@ -19,10 +29,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 +45,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) ──────────── diff --git a/.gitignore b/.gitignore index a9c8e26..b59be21 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,12 @@ backend/dev.db* .env !.env.example +# Сертификаты и приватные ключи (живут на VPS/Pi/ПК, не в репозитории) +*.pem +*.key +id_tunnel* +deploy/**/certs/ + # Node / фронт node_modules/ frontend/dist/ diff --git a/README.md b/README.md index 43ffe95..b2b83a0 100644 --- a/README.md +++ b/README.md @@ -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,24 @@ docker compose -f docker-compose.test.yml down -v # остановить и с Секреты (`.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-служба). + ## Дополнения и фракции | Дополнение | Фракции | diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 94113fd..59b8a45 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -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 diff --git a/backend/app/main.py b/backend/app/main.py index 83b224f..af78510 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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, diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 4e2cf21..a513b62 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -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="*" diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..1feda41 --- /dev/null +++ b/deploy/README.md @@ -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`). diff --git a/deploy/pi/README.md b/deploy/pi/README.md new file mode 100644 index 0000000..64239b7 --- /dev/null +++ b/deploy/pi/README.md @@ -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`. diff --git a/deploy/pi/forbidden-stars-tunnel.service b/deploy/pi/forbidden-stars-tunnel.service new file mode 100644 index 0000000..36c2151 --- /dev/null +++ b/deploy/pi/forbidden-stars-tunnel.service @@ -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 diff --git a/deploy/vps/Caddyfile b/deploy/vps/Caddyfile new file mode 100644 index 0000000..cc66ff5 --- /dev/null +++ b/deploy/vps/Caddyfile @@ -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 +} diff --git a/deploy/vps/README.md b/deploy/vps/README.md new file mode 100644 index 0000000..8064df9 --- /dev/null +++ b/deploy/vps/README.md @@ -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`. diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 963ca04..e6238d0 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -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, diff --git a/run.ps1 b/run.ps1 index 3a35703..368046a 100644 --- a/run.ps1 +++ b/run.ps1 @@ -1,11 +1,14 @@ #!/usr/bin/env pwsh # ╔═══════════════════════════════════════════════════════════════════════════╗ -# ║ Единый лаунчер dev / test. Что запускать — решает APP_ENV из корневого .env ║ +# ║ Единый лаунчер dev / test. APP_ENV из корневого .env решает, что запускать. ║ # ╚═══════════════════════════════════════════════════════════════════════════╝ # development → uvicorn --reload (бэк) + vite (фронт), нативно, в двух окнах # test → docker compose прод-клон (порт 8080) # production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi) # +# LOCAL_PUBLIC=vps дополнительно поднимает SSH-туннель на VPS, и dev/test становится +# виден снаружи на https://forbidden-stars.ru (нужен настроенный VPS, см. deploy/vps). +# # Запуск: .\run.ps1 # Если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned $ErrorActionPreference = "Stop" @@ -17,12 +20,38 @@ if (-not (Test-Path $envFile)) { 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() } +# Достаём значение ключа из .env (последнее присвоение побеждает; комментарии игнор). +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 +} + +$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) { "development" { @@ -50,6 +79,10 @@ switch ($appEnv) { 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 + if ($localPublic -eq "vps") { + Start-VpsTunnel 5173 + Write-Host " Публично: https://forbidden-stars.ru" -ForegroundColor Green + } } "test" { $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 " Логи: docker compose -f docker-compose.test.yml logs -f" 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" { Write-Host "production лаунчером не запускается — прод обособлен." -ForegroundColor Yellow diff --git a/run.sh b/run.sh index 88ac473..1f8deff 100644 --- a/run.sh +++ b/run.sh @@ -6,6 +6,8 @@ # test → docker compose прод-клон (порт 8080) # production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi) # +# LOCAL_PUBLIC=vps дополнительно поднимает SSH-туннель на VPS → https://forbidden-stars.ru +# # Запуск: ./run.sh (при необходимости: chmod +x run.sh) set -euo pipefail root="$(cd "$(dirname "$0")" && pwd)" @@ -13,11 +15,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,12 +49,13 @@ 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) @@ -39,6 +64,11 @@ case "$app_env" in 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" + if [ "$local_public" = "vps" ]; then + start_tunnel 8080 + echo " (туннель в фоне; Ctrl+C — закрыть туннель. Контейнер останавливается отдельно.)" + wait "$tunnel" 2>/dev/null || true + fi ;; production) echo "production лаунчером не запускается — прод обособлен." From 7e8b748caf5d9f4ab3d2d7f76d6a4b850b9b1366 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Wed, 17 Jun 2026 04:27:11 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=B5=D0=B2=D0=B0,=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=20=D1=82=D1=83=D0=BD=D0=B5=D0=BB=D1=8F=20=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 14 +++-- README.md | 22 ++++--- backend/tests/conftest.py | 1 + deploy/README.md | 38 +++++++---- deploy/pi/README.md | 66 ++++++++++--------- deploy/pi/forbidden-stars-tunnel.service | 27 -------- deploy/tunnel/.dockerignore | 2 + deploy/tunnel/Dockerfile | 8 +++ deploy/tunnel/tunnel.sh | 26 ++++++++ deploy/vps/README.md | 41 ++++++++---- docker-compose.test.yml | 35 ++++++++--- docker-compose.yml | 24 ++++++- run.ps1 | 80 ++++++++++++++---------- run.sh | 17 +++-- 14 files changed, 249 insertions(+), 152 deletions(-) delete mode 100644 deploy/pi/forbidden-stars-tunnel.service create mode 100644 deploy/tunnel/.dockerignore create mode 100644 deploy/tunnel/Dockerfile create mode 100644 deploy/tunnel/tunnel.sh diff --git a/.env.example b/.env.example index 3fe6022..78da8c3 100644 --- a/.env.example +++ b/.env.example @@ -12,15 +12,17 @@ # Прод-контейнер ИГНОРИРУЕТ это значение и всегда production. APP_ENV=development -# ─── ПУБЛИКАЦИЯ ЛОКАЛКИ ЧЕРЕЗ ДОМЕН (VPS-туннель) ───────────────────────────── -# Только для dev/test на твоём ПК. local — приложение лишь на localhost; vps — -# лаунчер дополнительно поднимает SSH-туннель на VPS, и dev/test виден снаружи на -# https://forbidden-stars.ru. Прод (Pi) сюда не относится — у него свой autossh-туннель. +# ─── ПУБЛИКАЦИЯ ЧЕРЕЗ ДОМЕН (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 -# Параметры туннеля (нужны только при LOCAL_PUBLIC=vps): +# Параметры 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, который слушает Caddy для forbidden-stars.ru +VPS_TUNNEL_PORT=9001 # слот VPS для dev-туннеля (forbidden-stars.ru); контейнеры: prod=9000, test=9001 # ─── АДМИНИСТРАТОР (вход по логину/паролю, отдельно от Telegram) ────────────── ADMIN_USERNAME=admin diff --git a/README.md b/README.md index b2b83a0..10aa7ce 100644 --- a/README.md +++ b/README.md @@ -187,20 +187,22 @@ docker compose -f docker-compose.test.yml down -v # остановить и с ## Домен и публикация Публичные адреса отдаёт **VPS-привратник** (Caddy + HTTPS твоими сертификатами), а -приложение само открывает к нему SSH-туннель (дома белого IP нет — CGNAT): +приложение само открывает к нему SSH reverse-туннель (дома белого IP нет — CGNAT). +У **test/prod** туннель — **отдельный контейнер** в их `docker-compose`, и портов на хост +они не публикуют (доступны только через домен): -| | домен | источник | туннель | +| | домен | как выставляется | слот VPS | |---|---|---|---| -| prod | `forbiddenstars.ru` | Pi, `app:8000` | autossh, постоянно (VPS:9000) | -| dev/test | `forbidden-stars.ru` | ПК, vite:5173 / app:8080 | `ssh -R`, по требованию (VPS:9001) | +| 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 | -- Прод и dev/test на **разных доменах** → могут работать одновременно. Dev и test делят - слот `9001` — по очереди. -- Публикация локалки: в `.env` поставить `LOCAL_PUBLIC=vps` и запустить `run.ps1` — лаунчер - сам поднимет SSH-туннель. `LOCAL_PUBLIC=local` — только localhost. +- Прод (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, сертификаты, туннель-юзер), - `pi/` (Docker-приложение + autossh-служба). +- Пошаговая настройка — в [`deploy/`](deploy/README.md): `vps/` (Caddy, сертификаты, юзер `tunnel`), + `pi/` (app + туннель в Docker), образ туннеля — `deploy/tunnel/`. Ключ — `deploy/tunnel/id_tunnel`. ## Дополнения и фракции diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index af004d8..8434995 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -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" # тесты сами поднимают схему/данные diff --git a/deploy/README.md b/deploy/README.md index 1feda41..e39409e 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -8,28 +8,40 @@ HTTPS твоими сертификатами и проксирует трафи ``` ┌──────────── 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)│ + │ ▲ контейнер (test) ИЛИ │ + │ ▲ ssh с ПК (dev) │ + │ ├── ПК test : app:8000 │ + │ └── ПК dev : vite:5173 │ └───────────────────────────────────────────────────┘ ``` -- **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 могут работать одновременно. +- **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): Docker-приложение + autossh-служба туннеля. -3. **ПК (dev/test)** — ключ для `tunnel@VPS` (`ssh-copy-id`), затем `LOCAL_PUBLIC=vps` + `run.ps1`. +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`, SSH-ключи) живут на VPS/Pi/ПК, -в репозитории только `Caddyfile` и юнит-шаблон. +Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, `id_tunnel*`) живут на VPS/Pi/ПК, +в репозитории только `Caddyfile`, `deploy/tunnel/` (образ туннеля) и шаблоны. > Telegram-вход требует HTTPS-домен: у BotFather `/setdomain` укажи оба домена > (`forbiddenstars.ru` и `forbidden-stars.ru`). diff --git a/deploy/pi/README.md b/deploy/pi/README.md index 64239b7..ef16b60 100644 --- a/deploy/pi/README.md +++ b/deploy/pi/README.md @@ -1,38 +1,46 @@ # Raspberry Pi — ПРОД (`forbiddenstars.ru`) -Pi запускает приложение в Docker и держит постоянный SSH reverse-туннель к VPS. -Снаружи Pi недоступен (CGNAT) — публичная точка только VPS, Pi сам стучится наружу. +Pi запускает приложение **и туннель** в Docker: `docker compose up` поднимает два сервиса — +`app` (FastAPI + SPA) и `tunnel` (ssh-контейнер). У `app` **портов на хост нет**, наружу он +доступен только через туннель-контейнер (`ssh -R 9000:app:8000` к VPS). Публичная точка — +VPS, домен `forbiddenstars.ru`. Снаружи Pi недоступен (CGNAT) — туннель сам стучится наружу. -## 1. Приложение (Docker) -Из ветки `main`: +## 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 -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 -``` +## Бэкап БД +`scripts/backup.sh` (консистентная онлайн-копия SQLite; по cron на хосте). Том БД — `db-data`. -Готово: `https://forbiddenstars.ru` отдаёт прод. Логи туннеля: `journalctl -u forbidden-stars-tunnel -f`. +Логи: `docker compose logs -f app` / `... tunnel`. Если `https://forbiddenstars.ru` отдаёт +502 — значит туннель не поднят: смотри `docker compose logs tunnel` (чаще всего ключ не в +`authorized_keys` на VPS или не заданы `VPS_TUNNEL_*` в `.env`). diff --git a/deploy/pi/forbidden-stars-tunnel.service b/deploy/pi/forbidden-stars-tunnel.service deleted file mode 100644 index 36c2151..0000000 --- a/deploy/pi/forbidden-stars-tunnel.service +++ /dev/null @@ -1,27 +0,0 @@ -# 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 diff --git a/deploy/tunnel/.dockerignore b/deploy/tunnel/.dockerignore new file mode 100644 index 0000000..e46edd6 --- /dev/null +++ b/deploy/tunnel/.dockerignore @@ -0,0 +1,2 @@ +# Приватный ключ монтируется в рантайме, в образ его НЕ кладём. +id_tunnel* diff --git a/deploy/tunnel/Dockerfile b/deploy/tunnel/Dockerfile new file mode 100644 index 0000000..f457745 --- /dev/null +++ b/deploy/tunnel/Dockerfile @@ -0,0 +1,8 @@ +# Крошечный контейнер SSH reverse-туннеля к VPS. +# Держит -R :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"] diff --git a/deploy/tunnel/tunnel.sh b/deploy/tunnel/tunnel.sh new file mode 100644 index 0000000..61b7fdf --- /dev/null +++ b/deploy/tunnel/tunnel.sh @@ -0,0 +1,26 @@ +#!/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}" + +# Копируем ключ и выставляем строгие права (с хоста права могут не годиться, особенно с 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}" diff --git a/deploy/vps/README.md b/deploy/vps/README.md index 8064df9..a546449 100644 --- a/deploy/vps/README.md +++ b/deploy/vps/README.md @@ -54,21 +54,36 @@ install -m 600 -o tunnel -g tunnel /dev/null /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/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 +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`, затем: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index eb8499c..a306d57 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -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: diff --git a/docker-compose.yml b/docker-compose.yml index 8e3409b..db82efe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/run.ps1 b/run.ps1 index 368046a..8909e74 100644 --- a/run.ps1 +++ b/run.ps1 @@ -1,26 +1,27 @@ #!/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) # -# LOCAL_PUBLIC=vps дополнительно поднимает SSH-туннель на VPS, и dev/test становится -# виден снаружи на https://forbidden-stars.ru (нужен настроенный VPS, см. deploy/vps). +# 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.ps1 -# Если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned +# 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 } -# Достаём значение ключа из .env (последнее присвоение побеждает; комментарии игнор). +# 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) { @@ -37,37 +38,51 @@ $vpsPort = Get-EnvVal "VPS_TUNNEL_PORT" "9001" Write-Host "APP_ENV = $appEnv LOCAL_PUBLIC = $localPublic" -ForegroundColor Cyan -# Поднимает SSH reverse-tunnel: VPS:$vpsPort → localhost:$localPort (в отдельном окне). +# 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, но VPS_TUNNEL_HOST не задан в .env — туннель пропущен." -ForegroundColor Red + Write-Host "LOCAL_PUBLIC=vps but VPS_TUNNEL_HOST is empty in .env - tunnel skipped." -ForegroundColor Red return } - Write-Host "SSH-туннель: forbidden-stars.ru (VPS:$vpsPort) → localhost:$localPort …" -ForegroundColor Green + 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-туннель на forbidden-stars.ru. Закрой окно, чтобы остановить.' -ForegroundColor Cyan; $ssh" + "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" { $backend = Join-Path $root "backend" $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" @@ -77,34 +92,31 @@ 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 " Публично: https://forbidden-stars.ru" -ForegroundColor Green + Write-Host " Public: https://forbidden-stars.ru" -ForegroundColor Green } } "test" { $compose = Join-Path $root "docker-compose.test.yml" - Write-Host "Сборка и запуск прод-клона в Docker (порт 8080)…" -ForegroundColor Green + 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" - if ($localPublic -eq "vps") { - Start-VpsTunnel 8080 - Write-Host " Публично: https://forbidden-stars.ru" -ForegroundColor Green - } + 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 } } diff --git a/run.sh b/run.sh index 1f8deff..96ffb46 100644 --- a/run.sh +++ b/run.sh @@ -6,7 +6,8 @@ # test → docker compose прод-клон (порт 8080) # production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi) # -# LOCAL_PUBLIC=vps дополнительно поднимает SSH-туннель на VPS → https://forbidden-stars.ru +# LOCAL_PUBLIC=vps (только dev) поднимает SSH-туннель на VPS → https://forbidden-stars.ru +# test/prod выставляют себя сами через туннель-контейнер (см. docker-compose*.yml). # # Запуск: ./run.sh (при необходимости: chmod +x run.sh) set -euo pipefail @@ -59,16 +60,12 @@ case "$app_env" in ( cd "$root/frontend" && npm run dev ) ;; test) - echo "Сборка и запуск прод-клона в Docker (порт 8080)…" + 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" - if [ "$local_public" = "vps" ]; then - start_tunnel 8080 - echo " (туннель в фоне; Ctrl+C — закрыть туннель. Контейнер останавливается отдельно.)" - wait "$tunnel" 2>/dev/null || true - fi + 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 лаунчером не запускается — прод обособлен." From 65bebf8b85fcbf921cbbc4f2599953a9fa3ca9c6 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Wed, 17 Jun 2026 04:45:33 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20ssh-=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=20?= =?UTF-8?q?=D0=BA=20=D0=BA=D0=BE=D0=BD=D1=82=D0=B5=D0=B9=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D1=83=20tunnel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++++ deploy/tunnel/tunnel.sh | 8 ++++++++ run.ps1 | 16 ++++++++++++++++ run.sh | 1 + 4 files changed, 29 insertions(+) diff --git a/.gitignore b/.gitignore index b59be21..a8293a5 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,10 @@ backend/openapi.json # Бэкапы (создаёт scripts/backup.sh на Pi) backups/ +# AI-ассистенты (локальные, в репозиторий не идут) +CLAUDE.md +.claude/ + # Редактор / ОС .DS_Store Thumbs.db diff --git a/deploy/tunnel/tunnel.sh b/deploy/tunnel/tunnel.sh index 61b7fdf..9ec8628 100644 --- a/deploy/tunnel/tunnel.sh +++ b/deploy/tunnel/tunnel.sh @@ -9,6 +9,14 @@ 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 diff --git a/run.ps1 b/run.ps1 index 8909e74..9bf026c 100644 --- a/run.ps1 +++ b/run.ps1 @@ -101,6 +101,22 @@ switch ($appEnv) { } "test" { $compose = Join-Path $root "docker-compose.test.yml" + 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 } diff --git a/run.sh b/run.sh index 96ffb46..ad90724 100644 --- a/run.sh +++ b/run.sh @@ -60,6 +60,7 @@ case "$app_env" in ( cd "$root/frontend" && npm run dev ) ;; test) + 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 " Публично: https://forbidden-stars.ru (Swagger: /api/docs)"