Правки локального размещения дева, перенос тунеля в контейнер
This commit is contained in:
+8
-6
@@ -12,15 +12,17 @@
|
|||||||
# Прод-контейнер ИГНОРИРУЕТ это значение и всегда production.
|
# Прод-контейнер ИГНОРИРУЕТ это значение и всегда production.
|
||||||
APP_ENV=development
|
APP_ENV=development
|
||||||
|
|
||||||
# ─── ПУБЛИКАЦИЯ ЛОКАЛКИ ЧЕРЕЗ ДОМЕН (VPS-туннель) ─────────────────────────────
|
# ─── ПУБЛИКАЦИЯ ЧЕРЕЗ ДОМЕН (VPS-туннель) ─────────────────────────────────────
|
||||||
# Только для dev/test на твоём ПК. local — приложение лишь на localhost; vps —
|
# LOCAL_PUBLIC — только для DEV на твоём ПК: local = приложение лишь на localhost;
|
||||||
# лаунчер дополнительно поднимает SSH-туннель на VPS, и dev/test виден снаружи на
|
# vps = лаунчер (run.ps1) дополнительно поднимает SSH-туннель → дев на forbidden-stars.ru.
|
||||||
# https://forbidden-stars.ru. Прод (Pi) сюда не относится — у него свой autossh-туннель.
|
# TEST и PROD выставляют себя сами через туннель-КОНТЕЙНЕР (docker-compose*.yml) — им
|
||||||
|
# LOCAL_PUBLIC не нужен, но VPS_TUNNEL_HOST/USER ниже они тоже читают.
|
||||||
LOCAL_PUBLIC=local
|
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_HOST=186.246.51.17
|
||||||
VPS_TUNNEL_USER=tunnel
|
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) ──────────────
|
# ─── АДМИНИСТРАТОР (вход по логину/паролю, отдельно от Telegram) ──────────────
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
|
|||||||
@@ -187,20 +187,22 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
|||||||
## Домен и публикация
|
## Домен и публикация
|
||||||
|
|
||||||
Публичные адреса отдаёт **VPS-привратник** (Caddy + HTTPS твоими сертификатами), а
|
Публичные адреса отдаёт **VPS-привратник** (Caddy + HTTPS твоими сертификатами), а
|
||||||
приложение само открывает к нему SSH-туннель (дома белого IP нет — CGNAT):
|
приложение само открывает к нему SSH reverse-туннель (дома белого IP нет — CGNAT).
|
||||||
|
У **test/prod** туннель — **отдельный контейнер** в их `docker-compose`, и портов на хост
|
||||||
|
они не публикуют (доступны только через домен):
|
||||||
|
|
||||||
| | домен | источник | туннель |
|
| | домен | как выставляется | слот VPS |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| prod | `forbiddenstars.ru` | Pi, `app:8000` | autossh, постоянно (VPS:9000) |
|
| prod | `forbiddenstars.ru` | туннель-контейнер (постоянно) → `app:8000` | 9000 |
|
||||||
| dev/test | `forbidden-stars.ru` | ПК, vite:5173 / app:8080 | `ssh -R`, по требованию (VPS:9001) |
|
| test | `forbidden-stars.ru` | туннель-контейнер → `app:8000` | 9001 |
|
||||||
|
| dev | `forbidden-stars.ru` | `run.ps1` при `LOCAL_PUBLIC=vps` → `localhost:5173` | 9001 |
|
||||||
|
|
||||||
- Прод и dev/test на **разных доменах** → могут работать одновременно. Dev и test делят
|
- Прод (9000) и dev/test (9001) на **разных слотах/доменах** → прод и (dev|test) работают
|
||||||
слот `9001` — по очереди.
|
одновременно. Dev и test делят слот 9001 → по очереди.
|
||||||
- Публикация локалки: в `.env` поставить `LOCAL_PUBLIC=vps` и запустить `run.ps1` — лаунчер
|
- Dev по умолчанию только на localhost; `LOCAL_PUBLIC=vps` + `run.ps1` выставляет его на домен.
|
||||||
сам поднимет SSH-туннель. `LOCAL_PUBLIC=local` — только localhost.
|
|
||||||
- `COOKIE_SECURE` выводится автоматически (HTTPS-домен ⇒ Secure-cookie; dev+localhost ⇒ нет).
|
- `COOKIE_SECURE` выводится автоматически (HTTPS-домен ⇒ Secure-cookie; dev+localhost ⇒ нет).
|
||||||
- Пошаговая настройка — в [`deploy/`](deploy/README.md): `vps/` (Caddy, сертификаты, туннель-юзер),
|
- Пошаговая настройка — в [`deploy/`](deploy/README.md): `vps/` (Caddy, сертификаты, юзер `tunnel`),
|
||||||
`pi/` (Docker-приложение + autossh-служба).
|
`pi/` (app + туннель в Docker), образ туннеля — `deploy/tunnel/`. Ключ — `deploy/tunnel/id_tunnel`.
|
||||||
|
|
||||||
## Дополнения и фракции
|
## Дополнения и фракции
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import os
|
|||||||
# обращение к app.db.session.engine не затронет файлы дева/прода, какой бы
|
# обращение к app.db.session.engine не затронет файлы дева/прода, какой бы
|
||||||
# APP_ENV ни был унаследован из окружения.
|
# APP_ENV ни был унаследован из окружения.
|
||||||
os.environ["APP_ENV"] = "development"
|
os.environ["APP_ENV"] = "development"
|
||||||
|
os.environ["LOCAL_PUBLIC"] = "local" # cookie_secure=False детерминированно (не зависит от .env разработчика)
|
||||||
os.environ["DEV_DATABASE_URL"] = "sqlite://"
|
os.environ["DEV_DATABASE_URL"] = "sqlite://"
|
||||||
os.environ["FS_STARTUP_BOOTSTRAP"] = "0" # тесты сами поднимают схему/данные
|
os.environ["FS_STARTUP_BOOTSTRAP"] = "0" # тесты сами поднимают схему/данные
|
||||||
|
|
||||||
|
|||||||
+25
-13
@@ -8,28 +8,40 @@ HTTPS твоими сертификатами и проксирует трафи
|
|||||||
```
|
```
|
||||||
┌──────────── VPS 186.246.51.17 (Caddy) ───────────┐
|
┌──────────── VPS 186.246.51.17 (Caddy) ───────────┐
|
||||||
forbiddenstars.ru ───►│ :443 (cert твой) → 127.0.0.1:9000 │
|
forbiddenstars.ru ───►│ :443 (cert твой) → 127.0.0.1:9000 │
|
||||||
│ ▲ autossh (постоянно) │
|
│ ▲ туннель-КОНТЕЙНЕР │
|
||||||
│ └── Pi : app:8000 PROD │
|
│ └── Pi : app:8000 PROD │
|
||||||
forbidden-stars.ru ──►│ :443 (cert твой) → 127.0.0.1:9001 │
|
forbidden-stars.ru ──►│ :443 (cert твой) → 127.0.0.1:9001 │
|
||||||
│ ▲ ssh (по требованию) │
|
│ ▲ контейнер (test) ИЛИ │
|
||||||
│ └── ПК : 5173(dev)/8080(test)│
|
│ ▲ ssh с ПК (dev) │
|
||||||
|
│ ├── ПК test : app:8000 │
|
||||||
|
│ └── ПК dev : vite:5173 │
|
||||||
└───────────────────────────────────────────────────┘
|
└───────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
- **PROD** — Pi, `forbiddenstars.ru`, туннель постоянный (autossh-служба). См. [`pi/`](pi/README.md).
|
- **PROD** — Pi. `docker compose up -d` поднимает два сервиса: `app` + `tunnel`. У `app`
|
||||||
- **DEV/TEST** — ПК, `forbidden-stars.ru`, туннель по требованию: в `.env` поставь
|
**портов на хост нет** — наружу его выставляет только туннель-контейнер
|
||||||
`LOCAL_PUBLIC=vps` и запусти `run.ps1` (dev → туннель на vite:5173; test → на :8080).
|
(`ssh -R 9000:app:8000` к VPS). Постоянно, Docker сам переподключает. См. [`pi/`](pi/README.md).
|
||||||
Dev и test делят порт `9001` — по очереди.
|
- **TEST** — ПК. То же самое: `docker compose -f docker-compose.test.yml up` поднимает
|
||||||
- Разделение окружений сохраняется на уровне приложения (свои `APP_ENV`, БД, контейнеры);
|
`app` + `tunnel` (`ssh -R 9001:app:8000`). Портов на хост нет — тест виден только на
|
||||||
домены разные, поэтому прод и dev/test могут работать одновременно.
|
`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`.
|
1. **VPS** — [`vps/README.md`](vps/README.md): Caddy, файрвол, пользователь `tunnel`, сертификаты, `Caddyfile`.
|
||||||
2. **Pi (прод)** — [`pi/README.md`](pi/README.md): Docker-приложение + autossh-служба туннеля.
|
2. **Pi (прод)** — [`pi/README.md`](pi/README.md): ключ в `deploy/tunnel/id_tunnel`, `.env`, `docker compose up -d`.
|
||||||
3. **ПК (dev/test)** — ключ для `tunnel@VPS` (`ssh-copy-id`), затем `LOCAL_PUBLIC=vps` + `run.ps1`.
|
3. **ПК (dev/test)** — тот же ключ в `deploy/tunnel/id_tunnel` (для test-контейнера) и/или
|
||||||
|
ключ по умолчанию для dev (`run.ps1`); pubkey — в `authorized_keys` у `tunnel@VPS`.
|
||||||
|
|
||||||
Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, SSH-ключи) живут на VPS/Pi/ПК,
|
Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, `id_tunnel*`) живут на VPS/Pi/ПК,
|
||||||
в репозитории только `Caddyfile` и юнит-шаблон.
|
в репозитории только `Caddyfile`, `deploy/tunnel/` (образ туннеля) и шаблоны.
|
||||||
|
|
||||||
> Telegram-вход требует HTTPS-домен: у BotFather `/setdomain` укажи оба домена
|
> Telegram-вход требует HTTPS-домен: у BotFather `/setdomain` укажи оба домена
|
||||||
> (`forbiddenstars.ru` и `forbidden-stars.ru`).
|
> (`forbiddenstars.ru` и `forbidden-stars.ru`).
|
||||||
|
|||||||
+37
-29
@@ -1,38 +1,46 @@
|
|||||||
# Raspberry Pi — ПРОД (`forbiddenstars.ru`)
|
# Raspberry Pi — ПРОД (`forbiddenstars.ru`)
|
||||||
|
|
||||||
Pi запускает приложение в Docker и держит постоянный SSH reverse-туннель к VPS.
|
Pi запускает приложение **и туннель** в Docker: `docker compose up` поднимает два сервиса —
|
||||||
Снаружи Pi недоступен (CGNAT) — публичная точка только VPS, Pi сам стучится наружу.
|
`app` (FastAPI + SPA) и `tunnel` (ssh-контейнер). У `app` **портов на хост нет**, наружу он
|
||||||
|
доступен только через туннель-контейнер (`ssh -R 9000:app:8000` к VPS). Публичная точка —
|
||||||
|
VPS, домен `forbiddenstars.ru`. Снаружи Pi недоступен (CGNAT) — туннель сам стучится наружу.
|
||||||
|
|
||||||
## 1. Приложение (Docker)
|
## 1. Ключ для туннеля
|
||||||
Из ветки `main`:
|
Туннель-контейнер ходит на `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
|
```bash
|
||||||
git pull
|
git pull
|
||||||
cp .env.example .env # заполни секреты; APP_ENV контейнер форсит в production
|
|
||||||
docker compose up -d --build
|
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
|
`scripts/backup.sh` (консистентная онлайн-копия SQLite; по cron на хосте). Том БД — `db-data`.
|
||||||
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`.
|
Логи: `docker compose logs -f app` / `... tunnel`. Если `https://forbiddenstars.ru` отдаёт
|
||||||
|
502 — значит туннель не поднят: смотри `docker compose logs tunnel` (чаще всего ключ не в
|
||||||
|
`authorized_keys` на VPS или не заданы `VPS_TUNNEL_*` в `.env`).
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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,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}"
|
||||||
+28
-13
@@ -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`).
|
Pi (`deploy/pi/README.md`) и ПК (через `ssh-copy-id`).
|
||||||
|
|
||||||
## 5. Сертификаты
|
## 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
|
```bash
|
||||||
mkdir -p /etc/caddy/certs/forbiddenstars.ru /etc/caddy/certs/forbidden-stars.ru
|
mkdir -p /etc/caddy/certs/forbidden-stars.ru /root/certs-tmp
|
||||||
```
|
# с локальной машины (пример для домена forbidden-stars.ru):
|
||||||
Залей свои файлы с локальной машины (с каждого домена — свой `fullchain.pem` + `privkey.pem`):
|
scp forbidden-stars.crt intermediate.crt forbidden-stars.key root@186.246.51.17:/root/certs-tmp/
|
||||||
```bash
|
# на 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 \
|
||||||
scp fullchain.pem privkey.pem root@186.246.51.17:/etc/caddy/certs/forbiddenstars.ru/
|
> /etc/caddy/certs/forbidden-stars.ru/fullchain.pem
|
||||||
scp fullchain.pem privkey.pem root@186.246.51.17:/etc/caddy/certs/forbidden-stars.ru/
|
cp /root/certs-tmp/forbidden-stars.key /etc/caddy/certs/forbidden-stars.ru/privkey.pem
|
||||||
```
|
# то же для forbiddenstars.ru (свои crt/intermediate/key), затем права
|
||||||
- `fullchain.pem` = твой сертификат + промежуточные GlobalSign **одним файлом** (сначала leaf, затем chain).
|
# (папкам нужен x-бит → 750, файлам 640; иначе пользователь caddy не прочитает):
|
||||||
- `privkey.pem` = приватный ключ (без пароля).
|
chown -R caddy:caddy /etc/caddy/certs
|
||||||
```bash
|
find /etc/caddy/certs -type d -exec chmod 750 {} \;
|
||||||
# на VPS — права:
|
find /etc/caddy/certs -type f -exec chmod 640 {} \;
|
||||||
chown -R caddy:caddy /etc/caddy/certs && chmod -R 640 /etc/caddy/certs
|
rm -rf /root/certs-tmp
|
||||||
```
|
```
|
||||||
|
> Если GlobalSign дал не отдельный промежуточный, а готовый bundle/chain — используй его
|
||||||
|
> как «промежуточную» часть. Если дал один файл, где leaf и chain уже вместе, — это и есть
|
||||||
|
> `fullchain.pem` (склейка не нужна).
|
||||||
|
|
||||||
## 6. Caddyfile
|
## 6. Caddyfile
|
||||||
Скопируй `deploy/vps/Caddyfile` из репозитория в `/etc/caddy/Caddyfile`, затем:
|
Скопируй `deploy/vps/Caddyfile` из репозитория в `/etc/caddy/Caddyfile`, затем:
|
||||||
|
|||||||
+27
-8
@@ -1,11 +1,10 @@
|
|||||||
# Локальный «клон прода» в контейнере — для проверки прод-сборки на своей машине
|
# Локальный «клон прода» в контейнере — для проверки прод-сборки на своей машине
|
||||||
# (а не на Raspberry Pi). Тот же образ, что и прод, но изолированные тома, единый .env
|
# (а не на Raspberry Pi). Тот же образ, что и прод, изолированные тома, единый .env.
|
||||||
# и порт 8080 (чтобы не конфликтовать с dev-uvicorn на :8000).
|
# Портов на хост НЕТ: тест виден ТОЛЬКО снаружи на https://forbidden-stars.ru через
|
||||||
|
# сервис tunnel (контейнер ssh -R 9001:app:8000 на VPS).
|
||||||
#
|
#
|
||||||
# Обычно запускается лаунчером при APP_ENV=test (run.ps1 / run.sh). Вручную:
|
# Запуск (обычно лаунчером при APP_ENV=test): docker compose -f docker-compose.test.yml up --build -d
|
||||||
# docker compose -f docker-compose.test.yml up --build -d
|
# Остановить и стереть данные: docker compose -f docker-compose.test.yml down -v
|
||||||
# Открыть: http://localhost:8080 (Swagger: /api/docs)
|
|
||||||
# Остановить и стереть данные: docker compose -f docker-compose.test.yml down -v
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
@@ -19,8 +18,8 @@ services:
|
|||||||
# Окружение test: прод-клон, но отличимый от прода (см. config.is_test).
|
# Окружение test: прод-клон, но отличимый от прода (см. config.is_test).
|
||||||
# Форсим здесь, чтобы значение не зависело от APP_ENV внутри .env.
|
# Форсим здесь, чтобы значение не зависело от APP_ENV внутри .env.
|
||||||
APP_ENV: test
|
APP_ENV: test
|
||||||
ports:
|
# Портов на хост НЕТ: тест доступен только изнутри сети compose; наружу — через
|
||||||
- "8080:8000"
|
# сервис tunnel (ниже) на forbidden-stars.ru. Из LAN/localhost — недоступно.
|
||||||
volumes:
|
volumes:
|
||||||
- db-data-test:/data # изолированные тестовые данные
|
- db-data-test:/data # изолированные тестовые данные
|
||||||
- uploads-data-test:/data/uploads
|
- uploads-data-test:/data/uploads
|
||||||
@@ -36,6 +35,26 @@ services:
|
|||||||
start_period: 40s
|
start_period: 40s
|
||||||
mem_limit: 512m
|
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:
|
volumes:
|
||||||
db-data-test:
|
db-data-test:
|
||||||
uploads-data-test:
|
uploads-data-test:
|
||||||
|
|||||||
+22
-2
@@ -10,8 +10,8 @@ services:
|
|||||||
# Прод обособлен: контейнер ВСЕГДА production и ИГНОРИРУЕТ APP_ENV из .env
|
# Прод обособлен: контейнер ВСЕГДА production и ИГНОРИРУЕТ APP_ENV из .env
|
||||||
# (хоть development, хоть test). БД=/data, STATIC_DIR=/app/static — из Dockerfile.
|
# (хоть development, хоть test). БД=/data, STATIC_DIR=/app/static — из Dockerfile.
|
||||||
APP_ENV: production
|
APP_ENV: production
|
||||||
ports:
|
# Портов на хост НЕТ: приложение доступно только изнутри сети compose.
|
||||||
- "8000:8000"
|
# Наружу его выставляет сервис tunnel (ниже) через VPS. Из LAN/localhost — недоступно.
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/data # БД SQLite + WAL-сайдкары
|
- db-data:/data # БД SQLite + WAL-сайдкары
|
||||||
- uploads-data:/data/uploads # задел под фото/видео
|
- uploads-data:/data/uploads # задел под фото/видео
|
||||||
@@ -33,6 +33,26 @@ services:
|
|||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "5"
|
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:
|
volumes:
|
||||||
db-data:
|
db-data:
|
||||||
uploads-data:
|
uploads-data:
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
#!/usr/bin/env pwsh
|
#!/usr/bin/env pwsh
|
||||||
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
# Unified launcher for dev / test. APP_ENV in the root .env decides what to run.
|
||||||
# ║ Единый лаунчер dev / test. APP_ENV из корневого .env решает, что запускать. ║
|
# development -> uvicorn --reload (backend) + vite (frontend), native, two windows
|
||||||
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
# test -> docker compose prod-clone (port 8080)
|
||||||
# development → uvicorn --reload (бэк) + vite (фронт), нативно, в двух окнах
|
# production -> NOT started by launcher (prod is separate: docker compose up -d on Pi)
|
||||||
# test → docker compose прод-клон (порт 8080)
|
|
||||||
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
|
|
||||||
#
|
#
|
||||||
# LOCAL_PUBLIC=vps дополнительно поднимает SSH-туннель на VPS, и dev/test становится
|
# LOCAL_PUBLIC=vps (dev only) opens an SSH tunnel to the VPS, exposing dev at
|
||||||
# виден снаружи на https://forbidden-stars.ru (нужен настроенный VPS, см. deploy/vps).
|
# https://forbidden-stars.ru. test/prod expose themselves via an in-container tunnel.
|
||||||
#
|
#
|
||||||
# Запуск: .\run.ps1
|
# Run: .\run.ps1
|
||||||
# Если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
# 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"
|
$ErrorActionPreference = "Stop"
|
||||||
$root = $PSScriptRoot
|
$root = $PSScriptRoot
|
||||||
|
|
||||||
$envFile = Join-Path $root ".env"
|
$envFile = Join-Path $root ".env"
|
||||||
if (-not (Test-Path $envFile)) {
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Достаём значение ключа из .env (последнее присвоение побеждает; комментарии игнор).
|
# Read a key from .env (last assignment wins; comments ignored).
|
||||||
function Get-EnvVal([string]$name, [string]$default) {
|
function Get-EnvVal([string]$name, [string]$default) {
|
||||||
$val = $default
|
$val = $default
|
||||||
foreach ($line in Get-Content $envFile) {
|
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
|
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) {
|
function Start-VpsTunnel([int]$localPort) {
|
||||||
if (-not $vpsHost) {
|
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
|
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} " +
|
$ssh = "ssh -N -R ${vpsPort}:localhost:${localPort} " +
|
||||||
"-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes " +
|
"-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes " +
|
||||||
"${vpsUser}@${vpsHost}"
|
"${vpsUser}@${vpsHost}"
|
||||||
Start-Process powershell -ArgumentList @(
|
Start-Process powershell -ArgumentList @(
|
||||||
"-NoExit", "-Command",
|
"-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) {
|
switch ($appEnv) {
|
||||||
"development" {
|
"development" {
|
||||||
$backend = Join-Path $root "backend"
|
$backend = Join-Path $root "backend"
|
||||||
$frontend = Join-Path $root "frontend"
|
$frontend = Join-Path $root "frontend"
|
||||||
$py = Join-Path $backend ".venv\Scripts\python.exe"
|
$py = Join-Path $backend ".venv\Scripts\python.exe"
|
||||||
if (-not (Test-Path $py)) {
|
if (-not (Test-Path $py)) {
|
||||||
Write-Host "Нет venv: $py" -ForegroundColor Red
|
Write-Host "venv not found: $py" -ForegroundColor Red
|
||||||
Write-Host "Сначала: cd backend; python -m venv .venv; .\.venv\Scripts\Activate.ps1; pip install -e `".[dev]`"" -ForegroundColor Yellow
|
Write-Host "First: cd backend; python -m venv .venv; .\.venv\Scripts\Activate.ps1; pip install -e `".[dev]`"" -ForegroundColor Yellow
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
if (-not (Test-Path (Join-Path $frontend "node_modules"))) {
|
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
|
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 @(
|
Start-Process powershell -ArgumentList @(
|
||||||
"-NoExit", "-Command",
|
"-NoExit", "-Command",
|
||||||
"Set-Location '$backend'; & '$py' -m uvicorn app.main:app --reload"
|
"Set-Location '$backend'; & '$py' -m uvicorn app.main:app --reload"
|
||||||
@@ -77,34 +92,31 @@ switch ($appEnv) {
|
|||||||
"Set-Location '$frontend'; npm run dev"
|
"Set-Location '$frontend'; npm run dev"
|
||||||
)
|
)
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " Бэк: http://127.0.0.1:8000 (Swagger: /api/docs)" -ForegroundColor Green
|
Write-Host " Backend: http://127.0.0.1:8000 (Swagger: /api/docs)" -ForegroundColor Green
|
||||||
Write-Host " Фронт: http://127.0.0.1:5173" -ForegroundColor Green
|
Write-Host " Frontend: http://127.0.0.1:5173" -ForegroundColor Green
|
||||||
if ($localPublic -eq "vps") {
|
if ($localPublic -eq "vps") {
|
||||||
Start-VpsTunnel 5173
|
Start-VpsTunnel 5173
|
||||||
Write-Host " Публично: https://forbidden-stars.ru" -ForegroundColor Green
|
Write-Host " Public: https://forbidden-stars.ru" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"test" {
|
"test" {
|
||||||
$compose = Join-Path $root "docker-compose.test.yml"
|
$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
|
docker compose -f $compose up --build -d
|
||||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " Открыть: http://localhost:8080 (Swagger: /api/docs)" -ForegroundColor Green
|
Write-Host " Public: https://forbidden-stars.ru (Swagger: /api/docs)" -ForegroundColor Green
|
||||||
Write-Host " Логи: docker compose -f docker-compose.test.yml logs -f"
|
Write-Host " (no host port - reachable only via the domain; tunnel runs inside compose)"
|
||||||
Write-Host " Стоп: docker compose -f docker-compose.test.yml down -v"
|
Write-Host " Logs: docker compose -f docker-compose.test.yml logs -f"
|
||||||
if ($localPublic -eq "vps") {
|
Write-Host " Stop: docker compose -f docker-compose.test.yml down -v"
|
||||||
Start-VpsTunnel 8080
|
|
||||||
Write-Host " Публично: https://forbidden-stars.ru" -ForegroundColor Green
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"production" {
|
"production" {
|
||||||
Write-Host "production лаунчером не запускается — прод обособлен." -ForegroundColor Yellow
|
Write-Host "production is not started by the launcher - prod is separate." -ForegroundColor Yellow
|
||||||
Write-Host "Деплой на Pi (из ветки main): docker compose up -d --build"
|
Write-Host "Deploy on Pi (from main branch): docker compose up -d --build"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
default {
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
# test → docker compose прод-клон (порт 8080)
|
# test → docker compose прод-клон (порт 8080)
|
||||||
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
|
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
|
||||||
#
|
#
|
||||||
# LOCAL_PUBLIC=vps дополнительно поднимает SSH-туннель на VPS → https://forbidden-stars.ru
|
# LOCAL_PUBLIC=vps (только dev) поднимает SSH-туннель на VPS → https://forbidden-stars.ru
|
||||||
|
# test/prod выставляют себя сами через туннель-контейнер (см. docker-compose*.yml).
|
||||||
#
|
#
|
||||||
# Запуск: ./run.sh (при необходимости: chmod +x run.sh)
|
# Запуск: ./run.sh (при необходимости: chmod +x run.sh)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -59,16 +60,12 @@ case "$app_env" in
|
|||||||
( cd "$root/frontend" && npm run dev )
|
( cd "$root/frontend" && npm run dev )
|
||||||
;;
|
;;
|
||||||
test)
|
test)
|
||||||
echo "Сборка и запуск прод-клона в Docker (порт 8080)…"
|
echo "Сборка и запуск прод-клона в Docker (app + туннель в контейнере)…"
|
||||||
docker compose -f "$root/docker-compose.test.yml" up --build -d
|
docker compose -f "$root/docker-compose.test.yml" up --build -d
|
||||||
echo " Открыть: http://localhost:8080 (Swagger: /api/docs)"
|
echo " Публично: https://forbidden-stars.ru (Swagger: /api/docs)"
|
||||||
echo " Логи: docker compose -f docker-compose.test.yml logs -f"
|
echo " (портов на хост нет — только через домен; туннель живёт внутри compose)"
|
||||||
echo " Стоп: docker compose -f docker-compose.test.yml down -v"
|
echo " Логи: docker compose -f docker-compose.test.yml logs -f"
|
||||||
if [ "$local_public" = "vps" ]; then
|
echo " Стоп: docker compose -f docker-compose.test.yml down -v"
|
||||||
start_tunnel 8080
|
|
||||||
echo " (туннель в фоне; Ctrl+C — закрыть туннель. Контейнер останавливается отдельно.)"
|
|
||||||
wait "$tunnel" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
production)
|
production)
|
||||||
echo "production лаунчером не запускается — прод обособлен."
|
echo "production лаунчером не запускается — прод обособлен."
|
||||||
|
|||||||
Reference in New Issue
Block a user