Документация: сверка README и deploy/*.md с кодом
- README: test без порта на хосте (только через домен, Secure-cookie), прод из реестра (build-push + docker compose up -d), лимиты перебора и регистраций, dev_admin.py в списке dev-кода, структура репозитория, раздел о бэкапах, отличия test от prod; отмечены известные проблемы (#69, #71, #72, #73). - deploy/README.md: три сервиса (app + tunnel + backup), источники ключа туннеля для Pi, test и dev-туннеля, слот 9000 у временного прода. - deploy/pi/README.md: контейнер backup, fail-fast по секретам, ADMIN_PASSWORD только при первом создании админа (#73), порядок обновления. - deploy/vps/README.md: туннель-контейнер вместо autossh, сниппет (edge), единые имена файлов в примере сборки сертификатов, дописывать authorized_keys через >>. - deploy/backup/README.md: первый бэкап на новом Pi, выбор снимка с данными при восстановлении (#74), метка keep, --no-pre-restore, служебные команды, причины unhealthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
This commit is contained in:
@@ -1,21 +1,28 @@
|
|||||||
# Forbidden Stars — учёт партий
|
# Forbidden Stars — учёт партий
|
||||||
|
|
||||||
Мобильное веб-приложение для учёта партий настольной игры **Forbidden Stars**:
|
Мобильное веб-приложение для учёта партий настольной игры **Forbidden Stars**:
|
||||||
профили игроков (вход по логину и паролю или через Telegram), группы, создание партий
|
профили игроков (вход по логину и паролю или через Telegram), группы с приглашениями,
|
||||||
с рандомом фракций и фильтром по дополнениям группы, статистика и общий топ, админ-панель.
|
создание партий с рандомом фракций и фильтром по дополнениям группы, фото партий,
|
||||||
|
уведомления, статистика и общий топ, админ-панель.
|
||||||
|
|
||||||
- **Бэкенд / ядро + API:** Python · FastAPI · SQLModel · SQLite
|
- **Бэкенд / ядро + API:** Python · FastAPI · SQLModel · SQLite
|
||||||
- **Фронтенд:** React · Vite · TypeScript (SPA, общается с ядром только по REST API)
|
- **Фронтенд:** React · Vite · TypeScript (SPA; с ядром общается по REST API, живые
|
||||||
- **Хостинг:** Raspberry Pi 4 (ARM64) в Docker
|
обновления приходят SSE-потоком `/api/events`)
|
||||||
|
- **Хостинг:** Raspberry Pi 4 (ARM64) в Docker; наружу — через VPS-привратник (см. «Домен и публикация»)
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
```
|
```
|
||||||
backend/ FastAPI: ядро, REST API, БД, миграции, seed
|
backend/ FastAPI: ядро, REST API, БД, миграции Alembic, seed, тесты
|
||||||
frontend/ React + Vite SPA
|
frontend/ React + Vite SPA
|
||||||
Dockerfile multi-stage сборка (фронт собирается node, отдаётся FastAPI)
|
deploy/ публикация и эксплуатация: vps/ (Caddy), pi/ (прод), tunnel/ и backup/ (образы)
|
||||||
docker-compose.yml
|
scripts/ build-push.* (сборка и пуш образов), fs-backup.* (бэкапы с ПК), export-*.sh
|
||||||
.env.example
|
Dockerfile multi-stage сборка (фронт собирается node, отдаётся FastAPI)
|
||||||
|
docker-compose.yml прод на Pi: app + tunnel + backup
|
||||||
|
docker-compose.test.yml тест-клон прода на ПК: app + tunnel + backup
|
||||||
|
docker-compose.temp.yml временный прод на ПК вместо Pi: app + tunnel
|
||||||
|
run.ps1 / run.sh единый лаунчер dev/test
|
||||||
|
.env.example шаблон единого .env
|
||||||
```
|
```
|
||||||
|
|
||||||
## Локальная разработка
|
## Локальная разработка
|
||||||
@@ -31,9 +38,9 @@ docker-compose.yml
|
|||||||
|
|
||||||
| `APP_ENV` в `.env` | что делает лаунчер |
|
| `APP_ENV` в `.env` | что делает лаунчер |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `development` | `uvicorn --reload` (бэк) + `vite` (фронт) нативно, в двух окнах |
|
| `development` | сначала `alembic upgrade head`, затем `uvicorn --reload` (бэк) + `vite` (фронт) нативно: `run.ps1` — в отдельных окнах, `run.sh` — в текущем терминале (Ctrl+C останавливает оба). При `LOCAL_PUBLIC=vps` дополнительно поднимает SSH-туннель на `forbidden-stars.ru` |
|
||||||
| `test` | `docker compose` прод-клон на :8080 (со сборкой образа) |
|
| `test` | `docker compose -f docker-compose.test.yml up --build -d` — прод-клон (app + tunnel + backup); портов на хост нет, открывается на `https://forbidden-stars.ru` |
|
||||||
| `production` | не запускает — прод деплоится отдельно (см. «Git и деплой») |
|
| `production` | не запускает — прод деплоится отдельно (см. «Production» и «Git и деплой») |
|
||||||
|
|
||||||
Разовая настройка перед первым запуском — поднять venv бэка и зависимости фронта
|
Разовая настройка перед первым запуском — поднять venv бэка и зависимости фронта
|
||||||
(после неё повседневный цикл — просто `.\run.ps1`). Vite проксирует `/api` на бэкенд.
|
(после неё повседневный цикл — просто `.\run.ps1`). Vite проксирует `/api` на бэкенд.
|
||||||
@@ -47,8 +54,8 @@ python -m venv .venv
|
|||||||
.\.venv\Scripts\Activate.ps1 # если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
.\.venv\Scripts\Activate.ps1 # если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
|
||||||
pip install -e ".[dev]" # .[dev] — один аргумент (пакет + dev-зависимости)
|
pip install -e ".[dev]" # .[dev] — один аргумент (пакет + dev-зависимости)
|
||||||
Copy-Item ..\.env.example ..\.env # ЕДИНЫЙ .env лежит в КОРНЕ репозитория
|
Copy-Item ..\.env.example ..\.env # ЕДИНЫЙ .env лежит в КОРНЕ репозитория
|
||||||
alembic upgrade head # применит миграции и сидинг
|
alembic upgrade head # применит миграции и сидинг справочников
|
||||||
python -m app.bootstrap # создаст/синхронизирует администратора из .env
|
python -m app.bootstrap # справочники + создаст/синхронизирует администратора из .env (опционально)
|
||||||
uvicorn app.main:app --reload --timeout-graceful-shutdown 2 # http://localhost:8000 (Swagger: /api/docs)
|
uvicorn app.main:app --reload --timeout-graceful-shutdown 2 # http://localhost:8000 (Swagger: /api/docs)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -75,6 +82,10 @@ python -m app.bootstrap
|
|||||||
uvicorn app.main:app --reload --timeout-graceful-shutdown 2
|
uvicorn app.main:app --reload --timeout-graceful-shutdown 2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> В dev тот же bootstrap (справочники + админ из `.env`) выполняется сам при старте uvicorn,
|
||||||
|
> поэтому `python -m app.bootstrap` вручную обычно не нужен. Dev-БД, загрузки и ачивки
|
||||||
|
> лежат в `backend/data/dev/` (пути в `.env` относительные — запускайте из `backend/`).
|
||||||
|
|
||||||
> **`--timeout-graceful-shutdown` обязателен.** Открытая вкладка держит SSE-поток
|
> **`--timeout-graceful-shutdown` обязателен.** Открытая вкладка держит SSE-поток
|
||||||
> `/api/events`, и без лимита `--reload` ждёт его закрытия вечно — сайт висит на загрузке,
|
> `/api/events`, и без лимита `--reload` ждёт его закрытия вечно — сайт висит на загрузке,
|
||||||
> а в логе только `Reloading...`.
|
> а в логе только `Reloading...`.
|
||||||
@@ -96,35 +107,57 @@ npm run dev # http://127.0.0.1:5173 или http://localhost:5
|
|||||||
|
|
||||||
## Production (Docker на Pi)
|
## Production (Docker на Pi)
|
||||||
|
|
||||||
|
На Pi нужны только **два файла** — `docker-compose.yml` и `.env`: образы `app`, `tunnel` и
|
||||||
|
`backup` собираются на ПК под arm64 и пушатся в Gitea-реестр, Pi тянет их сам.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# ПК (обычно с ветки main): собрать и опубликовать образы
|
||||||
|
docker login gitea.arseniev.info
|
||||||
|
.\scripts\build-push.ps1
|
||||||
|
```
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # заполните SECRET_KEY, ADMIN_PASSWORD и пр.
|
# Pi, папка с docker-compose.yml и .env
|
||||||
docker compose build # на ARM64 собирается нативно
|
docker compose up -d # pull_policy: always — тянет свежие образы, без сборки
|
||||||
docker compose up -d # приложение на :8000, БД на томе
|
|
||||||
```
|
```
|
||||||
|
|
||||||
FastAPI отдаёт собранный SPA и API с одного origin. Миграции и бутстрап админа
|
Портов на хост нет — прод доступен только на `https://forbiddenstars.ru` через
|
||||||
выполняются автоматически при старте (`entrypoint.sh`).
|
туннель-контейнер. FastAPI отдаёт собранный SPA и API с одного origin. Миграции, сидинг
|
||||||
|
справочников и создание админа выполняются автоматически при старте (`entrypoint.sh`).
|
||||||
|
В production закрыты OpenAPI/Swagger, а с дефолтным или коротким `SECRET_KEY` либо
|
||||||
|
дефолтным `ADMIN_PASSWORD` приложение не стартует. Учтите: `ADMIN_PASSWORD` применяется только
|
||||||
|
при первом создании админа, дальше его смена в `.env` ни на что не влияет (задача #73).
|
||||||
|
Пошагово — [`deploy/pi/README.md`](deploy/pi/README.md), бэкапы — [`deploy/backup/README.md`](deploy/backup/README.md).
|
||||||
|
|
||||||
## Test — локальный прод-клон в контейнере
|
## Test — прод-клон в контейнере на ПК
|
||||||
|
|
||||||
Тот же образ и поведение, что и прод (FastAPI отдаёт SPA, БД на томе, вход игроков
|
Тот же `Dockerfile` и поведение, что у прода (FastAPI отдаёт SPA, БД на томе, вход игроков
|
||||||
по логину/паролю или через Telegram), но на своей машине — для проверки прод-сборки до выката на Pi.
|
по логину/паролю или через Telegram), но образ собирается локально — для проверки прод-сборки
|
||||||
Изолированные тома и порт **8080** (не конфликтует с dev-uvicorn на :8000).
|
до выката на Pi. Портов на хост **нет**: тест-клон виден только на `https://forbidden-stars.ru`
|
||||||
|
через свой туннель-контейнер (ключ — файл `deploy/tunnel/id_tunnel`).
|
||||||
|
|
||||||
Проще всего — через лаунчер: поставить `APP_ENV=test` в `.env` и запустить `.\run.ps1`.
|
Проще всего — через лаунчер: поставить `APP_ENV=test` в `.env` и запустить `.\run.ps1`.
|
||||||
Вручную (тот же эффект):
|
Вручную (тот же эффект):
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.test.yml up -d --build
|
docker compose -f docker-compose.test.yml up -d --build
|
||||||
# открыть http://localhost:8080 (Swagger: /api/docs)
|
# открыть https://forbidden-stars.ru (Swagger: /api/docs)
|
||||||
|
docker compose -f docker-compose.test.yml logs -f app
|
||||||
docker compose -f docker-compose.test.yml down -v # остановить и стереть тестовые данные
|
docker compose -f docker-compose.test.yml down -v # остановить и стереть тестовые данные
|
||||||
```
|
```
|
||||||
|
|
||||||
- Окружение `test` (прод-клон), но `COOKIE_SECURE=false` (локально по HTTP).
|
|
||||||
- Читает **тот же `.env`**, что dev/prod (отдельного `.env.test` больше нет); внутри
|
- Читает **тот же `.env`**, что dev/prod (отдельного `.env.test` больше нет); внутри
|
||||||
контейнера `APP_ENV` форсится в `test` (см. `docker-compose.test.yml`).
|
контейнера `APP_ENV` форсится в `test` (см. `docker-compose.test.yml`). Cookie — Secure
|
||||||
- Данные — на отдельных томах `db-data-test` / `uploads-data-test` (не пересекаются с dev и Pi).
|
(снаружи HTTPS).
|
||||||
- Вход: **админ-панель** (`/admin/login`) работает сразу по логину/паролю; **игроки** —
|
- От прода test отличается тем, что OpenAPI/Swagger открыт и **нет** fail-fast по дефолтным
|
||||||
по логину/паролю сразу, через Telegram — при боте и публичном HTTPS/туннеле на `localhost:8080`.
|
секретам, хотя контур публичный (задача #69). Не держите в `.env` дефолтные `SECRET_KEY` /
|
||||||
|
`ADMIN_PASSWORD`, когда поднимаете test, — особенно после `restore-test` с прод-данными.
|
||||||
|
- Данные — на отдельных томах `db-data-test` / `uploads-data-test` / `achievements-data-test`
|
||||||
|
(и `backup-data-test` у контейнера бэкапов, он работает без расписания и без VPS);
|
||||||
|
с dev и Pi не пересекаются.
|
||||||
|
- Слот VPS 9001 общий с dev-туннелем (`LOCAL_PUBLIC=vps`) — поднимайте что-то одно.
|
||||||
|
- Вход: **админ-панель** (`/admin/login`) работает сразу по логину/паролю; **игроки** — по
|
||||||
|
логину/паролю сразу, через Telegram — при настроенном боте (`/setdomain` → `forbidden-stars.ru`).
|
||||||
|
- Учебное восстановление прод-бэкапа в тест-клон — `.\scripts\fs-backup.ps1 restore-test`
|
||||||
|
([`deploy/backup/README.md`, шаг 7](deploy/backup/README.md#шаг-7-учебное-восстановление-на-тест-клоне)).
|
||||||
- **Не используйте `$` в секретах.** Единый `.env` читают и pydantic (dev — `$` дословно),
|
- **Не используйте `$` в секретах.** Единый `.env` читают и pydantic (dev — `$` дословно),
|
||||||
и docker compose (test/prod — `$` = подстановка переменной). Чтобы значение совпадало
|
и docker compose (test/prod — `$` = подстановка переменной). Чтобы значение совпадало
|
||||||
везде, в `SECRET_KEY`/`ADMIN_PASSWORD` не должно быть `$`. Удобно генерировать так:
|
везде, в `SECRET_KEY`/`ADMIN_PASSWORD` не должно быть `$`. Удобно генерировать так:
|
||||||
@@ -142,17 +175,25 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
|||||||
|
|
||||||
- **Логин и пароль** (`app/auth/password.py`) — основной вход. Логин — это ник игрока
|
- **Логин и пароль** (`app/auth/password.py`) — основной вход. Логин — это ник игрока
|
||||||
(смена ника меняет логин). Пароль: от 8 символов, не длиннее 72 байт, хранится bcrypt.
|
(смена ника меняет логин). Пароль: от 8 символов, не длиннее 72 байт, хранится bcrypt.
|
||||||
От перебора — окно 15 минут в памяти процесса: 5 неудач на пару «IP + логин» и 20 на IP,
|
От перебора — окно 15 минут в памяти процесса: 5 неудач на пару «IP + логин», 20 на IP и
|
||||||
дальше `429 TOO_MANY_ATTEMPTS`. Игрок без пароля (из Telegram или созданный до паролей)
|
50 на аккаунт (независимо от IP), дальше `429 TOO_MANY_ATTEMPTS`. Регистраций — не больше
|
||||||
после входа видит обязательное окно «Задайте пароль». В профиле пароль меняется (нужен
|
10 с одного IP за окно. Игрок без пароля (из Telegram или созданный до паролей) после входа
|
||||||
текущий) и привязывается Telegram (ник не меняется). Забытый пароль задаёт админ
|
видит обязательное окно «Задайте пароль»: закрыть его нельзя, только задать пароль или
|
||||||
на вкладке аккаунтов — почту приложение не хранит.
|
выйти; в dev-сборке есть кнопка «Позже (dev)». В профиле пароль меняется (нужен текущий) и
|
||||||
- **Stub-вход (по нику)** — только для разработки. Его код **физически не попадает в прод:**
|
привязывается Telegram (ник не меняется). Забытый пароль задаёт админ на вкладке
|
||||||
файлы `backend/app/auth/dev_stub.py` и `backend/app/routers/dev_auth.py` исключены из
|
аккаунтов — почту приложение не хранит. Смена или сброс пароля завершает все прежние
|
||||||
Docker-образа (`.dockerignore`), роутер подключается лишь при `APP_ENV=development`
|
сессии игрока (при смене в профиле текущее устройство остаётся в системе); «Выйти»
|
||||||
(`app/main.py`), а на фронте dev-блок вырезается из прод-сборки (`import.meta.env.DEV`).
|
отзывает токен этого устройства.
|
||||||
- **Telegram:** сервер проверяет подпись виджета (HMAC по `TELEGRAM_BOT_TOKEN`).
|
- **Stub-вход (по нику)** и **жёсткое удаление аккаунтов** в админке — только для разработки.
|
||||||
`GET /api/auth/config` отдаёт доступные методы и `telegram_bot_username` для виджета.
|
Их код **физически не попадает в прод:** файлы `backend/app/auth/dev_stub.py`,
|
||||||
|
`backend/app/routers/dev_auth.py` и `backend/app/routers/dev_admin.py` исключены из
|
||||||
|
Docker-образа (`.dockerignore`), роутеры подключаются лишь при `APP_ENV=development`
|
||||||
|
(`app/main.py`), а на фронте dev-блоки вырезаются из прод-сборки (`import.meta.env.DEV`).
|
||||||
|
В test/prod аккаунт можно только отключить.
|
||||||
|
- **Telegram:** сервер проверяет подпись виджета (HMAC по `TELEGRAM_BOT_TOKEN`) и свежесть
|
||||||
|
данных (не старше суток). Первый вход регистрирует игрока под Telegram-тегом; если такой ник
|
||||||
|
занят или некорректен, фронт просит выбрать другой. `GET /api/auth/config` отдаёт доступные
|
||||||
|
методы и `telegram_bot_username` для виджета.
|
||||||
|
|
||||||
**Настройка Telegram (когда будете подключать реальный вход):**
|
**Настройка Telegram (когда будете подключать реальный вход):**
|
||||||
1. Создать бота у [@BotFather](https://t.me/BotFather) → получить **токен** и **username**.
|
1. Создать бота у [@BotFather](https://t.me/BotFather) → получить **токен** и **username**.
|
||||||
@@ -160,7 +201,8 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
|||||||
3. В `.env`: `TELEGRAM_BOT_TOKEN=...`, `TELEGRAM_BOT_USERNAME=...` (без `@`).
|
3. В `.env`: `TELEGRAM_BOT_TOKEN=...`, `TELEGRAM_BOT_USERNAME=...` (без `@`).
|
||||||
4. Виджету нужен HTTPS-домен (см. «Домен и публикация») — по голому HTTP/localhost он не работает.
|
4. Виджету нужен HTTPS-домен (см. «Домен и публикация») — по голому HTTP/localhost он не работает.
|
||||||
|
|
||||||
Админ-вход (секретная панель, логин+пароль) — отдельный механизм, доступен во всех окружениях.
|
Админ-вход (секретная панель, логин+пароль) — отдельный механизм, доступен во всех
|
||||||
|
окружениях. Страница — `/admin/login`; из интерфейса туда ведёт удержание кнопки «Меню» 10 секунд.
|
||||||
|
|
||||||
## Окружения (dev / test / prod)
|
## Окружения (dev / test / prod)
|
||||||
|
|
||||||
@@ -171,18 +213,23 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
|||||||
| Запуск | `.\run.ps1` → `uvicorn --reload` + `vite` | `.\run.ps1` → `docker compose -f docker-compose.test.yml` | `docker compose up -d` |
|
| Запуск | `.\run.ps1` → `uvicorn --reload` + `vite` | `.\run.ps1` → `docker compose -f docker-compose.test.yml` | `docker compose up -d` |
|
||||||
| `APP_ENV` | `development` | `test` (форсится в compose) | `production` (форсится в compose) |
|
| `APP_ENV` | `development` | `test` (форсится в compose) | `production` (форсится в compose) |
|
||||||
| Env-файл | единый `.env` | единый `.env` | единый `.env` (на Pi) |
|
| Env-файл | единый `.env` | единый `.env` | единый `.env` (на Pi) |
|
||||||
| Раздача SPA | Vite (HMR), :5173 | FastAPI, :8080 | FastAPI, :8000 |
|
| Раздача SPA | Vite (HMR), `:5173` | FastAPI, только через `https://forbidden-stars.ru` | FastAPI, только через `https://forbiddenstars.ru` |
|
||||||
| База данных | `backend/data/dev/…` | том `db-data-test` (`/data`) | том `db-data` (`/data`) |
|
| База данных | `backend/data/dev/…` | том `db-data-test` (`/data`) | том `db-data` (`/data`) |
|
||||||
| Вход игроков | пароль + Telegram + ник (stub) | пароль + Telegram | пароль + Telegram |
|
| Вход игроков | пароль + Telegram + ник (stub) | пароль + Telegram | пароль + Telegram |
|
||||||
|
| Swagger (`/api/docs`) | ✓ | ✓ | ✗ |
|
||||||
|
| Fail-fast по дефолтным секретам | ✗ | ✗ | ✓ |
|
||||||
|
|
||||||
- **Один `.env` на машину** в корне (рядом с `.env.example`). `APP_ENV` в нём решает, что
|
- **Один `.env` на машину** в корне (рядом с `.env.example`). `APP_ENV` в нём решает, что
|
||||||
запустит лаунчер (`development`/`test`); прод-контейнер это значение **игнорирует** и всегда
|
запустит лаунчер (`development`/`test`); прод-контейнер это значение **игнорирует** и всегда
|
||||||
`production`. Отдельного `.env.test` больше нет.
|
`production`. Отдельного `.env.test` больше нет.
|
||||||
- **Структура БД одна** (общие миграции Alembic), **файлы разные**: dev → `DEV_DATABASE_URL`
|
- **Структура БД одна** (общие миграции Alembic), **файлы разные**: dev → `DEV_DATABASE_URL`
|
||||||
(`backend/data/dev/`), test и prod → `PROD_DATABASE_URL` (том `/data`; у test и prod это
|
(`backend/data/dev/`), test и prod → `PROD_DATABASE_URL` (том `/data`; у test и prod это
|
||||||
РАЗНЫЕ тома). Данные дева в образ **не попадают** (`data/` в `.dockerignore`).
|
РАЗНЫЕ тома). Так же раздельно лежат загрузки (`*_UPLOAD_DIR`) и ачивки (`*_ACHIEVEMENTS_DIR`).
|
||||||
- **В Docker идёт только прод-код:** dev-вход (stub) и тесты физически исключены из образа
|
- **В Docker идёт только прод-код:** dev-вход (stub), dev-удаление аккаунтов и тесты физически
|
||||||
(`.dockerignore`); `test` — тот же образ, что и прод, просто локально и с `APP_ENV=test`.
|
исключены из образа (`.dockerignore`); `test` собирается из того же `Dockerfile`, что и прод,
|
||||||
|
просто локально и с `APP_ENV=test`.
|
||||||
|
- **Внимание:** данные дева (`backend/data/dev/`) сейчас **попадают** в образ — правило
|
||||||
|
`data/` в `.dockerignore` исключает только корневую папку `data/` (задача #71).
|
||||||
|
|
||||||
## Git и деплой
|
## Git и деплой
|
||||||
|
|
||||||
@@ -190,9 +237,14 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
|||||||
- Ветка **`main`** — релиз прода: готовое промоутишь из `dev` через `merge dev→main`. Файлы во
|
- Ветка **`main`** — релиз прода: готовое промоутишь из `dev` через `merge dev→main`. Файлы во
|
||||||
всех ветках одинаковы (окружение задаёт `.env`/compose, а не ветка) → merge безболезненный;
|
всех ветках одинаковы (окружение задаёт `.env`/compose, а не ветка) → merge безболезненный;
|
||||||
чистоту прод-образа обеспечивает `.dockerignore`, а не разные наборы файлов.
|
чистоту прод-образа обеспечивает `.dockerignore`, а не разные наборы файлов.
|
||||||
- **Деплой на Pi:** `git pull` ветки `main` → `scripts/build-push.ps1`.
|
- **Деплой на Pi:** на ПК с ветки `main` — `.\scripts\build-push.ps1` (собирает и пушит
|
||||||
- **Чистая выгрузка в папку без git** (опц.): `scripts/export-prod.sh <dir> main` (через
|
образы app + tunnel + backup под arm64), на Pi — `docker compose up -d`. На Windows нужна
|
||||||
`git archive` + `export-ignore` — в папку идёт ровно прод-набор, без лаунчера/тестов/dev-входа).
|
именно PS-версия скрипта (`build-push.sh` из PowerShell уходит в WSL).
|
||||||
|
- **Чистая выгрузка в папку без git** (опц., к деплою на Pi не относится):
|
||||||
|
`scripts/export-prod.sh <dir> [ref]` / `scripts/export-test.sh <dir> [ref]` — через
|
||||||
|
`git archive` + `export-ignore` из `.gitattributes` (без тестов, stub-входа, `pyproject.toml`,
|
||||||
|
README-файлов; у прода ещё без лаунчера и тест-compose, у теста — без прод-compose). `dev_admin.py` в
|
||||||
|
`export-ignore` пока не внесён (задача #70).
|
||||||
|
|
||||||
Секреты (`.env`) и данные (`data/`, `*.db`) в git не идут — см. `.gitignore`.
|
Секреты (`.env`) и данные (`data/`, `*.db`) в git не идут — см. `.gitignore`.
|
||||||
|
|
||||||
@@ -207,14 +259,25 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| prod | `forbiddenstars.ru` | туннель-контейнер (постоянно) → `app:8000` | 9000 |
|
| prod | `forbiddenstars.ru` | туннель-контейнер (постоянно) → `app:8000` | 9000 |
|
||||||
| test | `forbidden-stars.ru` | туннель-контейнер → `app:8000` | 9001 |
|
| test | `forbidden-stars.ru` | туннель-контейнер → `app:8000` | 9001 |
|
||||||
| dev | `forbidden-stars.ru` | `run.ps1` при `LOCAL_PUBLIC=vps` → `localhost:5173` | 9001 |
|
| dev | `forbidden-stars.ru` | лаунчер (`run.ps1`/`run.sh`) при `LOCAL_PUBLIC=vps` → `localhost:5173` | 9001 |
|
||||||
|
|
||||||
- Прод (9000) и dev/test (9001) на **разных слотах/доменах** → прод и (dev|test) работают
|
- Прод (9000) и dev/test (9001) на **разных слотах/доменах** → прод и (dev|test) работают
|
||||||
одновременно. Dev и test делят слот 9001 → по очереди.
|
одновременно. Dev и test делят слот 9001 → по очереди. Временный прод на ПК
|
||||||
- Dev по умолчанию только на localhost; `LOCAL_PUBLIC=vps` + `run.ps1` выставляет его на домен.
|
(`docker-compose.temp.yml`) занимает слот 9000 — одновременно с продом на Pi не запускать.
|
||||||
|
- Dev по умолчанию только на localhost; `LOCAL_PUBLIC=vps` + лаунчер выставляет его на домен.
|
||||||
- `COOKIE_SECURE` выводится автоматически (HTTPS-домен ⇒ Secure-cookie; dev+localhost ⇒ нет).
|
- `COOKIE_SECURE` выводится автоматически (HTTPS-домен ⇒ Secure-cookie; dev+localhost ⇒ нет).
|
||||||
|
- Ключ туннеля: test и временный прод берут файл `deploy/tunnel/id_tunnel`, прод на Pi —
|
||||||
|
`TUNNEL_KEY_B64` (base64) в `.env`, dev-туннель — системный `ssh` с ключом по умолчанию
|
||||||
|
(`~/.ssh`). Публичные части — в `authorized_keys` пользователя `tunnel` на VPS.
|
||||||
- Пошаговая настройка — в [`deploy/`](deploy/README.md): `vps/` (Caddy, сертификаты, юзер `tunnel`),
|
- Пошаговая настройка — в [`deploy/`](deploy/README.md): `vps/` (Caddy, сертификаты, юзер `tunnel`),
|
||||||
`pi/` (app + туннель в Docker), образ туннеля — `deploy/tunnel/`. Ключ — `deploy/tunnel/id_tunnel`.
|
`pi/` (app + туннель + бэкапы в Docker), образ туннеля — `deploy/tunnel/`.
|
||||||
|
|
||||||
|
## Бэкапы
|
||||||
|
|
||||||
|
Контейнер `backup` (restic) в `docker-compose.yml` каждую ночь делает зашифрованный снимок
|
||||||
|
БД, `uploads` и `achievements` — на Pi (том `backup-data`) и на VPS по SFTP. С ПК снимки
|
||||||
|
скачиваются и проверяются учебным восстановлением в тест-клон (`scripts/fs-backup.ps1`).
|
||||||
|
Настройка, восстановление и действия при гибели Pi — [`deploy/backup/README.md`](deploy/backup/README.md).
|
||||||
|
|
||||||
## Дополнения и фракции
|
## Дополнения и фракции
|
||||||
|
|
||||||
@@ -225,4 +288,6 @@ docker compose -f docker-compose.test.yml down -v # остановить и с
|
|||||||
| Forsaken Voids | Инквизиция, Сёстры битвы, Друкхари, Адептус Механикус |
|
| Forsaken Voids | Инквизиция, Сёстры битвы, Друкхари, Адептус Механикус |
|
||||||
|
|
||||||
Группа отмечает имеющиеся дополнения; при создании партии доступны только фракции
|
Группа отмечает имеющиеся дополнения; при создании партии доступны только фракции
|
||||||
этих дополнений (база — всегда).
|
этих дополнений (база — всегда). Справочник сидится из `backend/app/seed/reference_data.py`.
|
||||||
|
Админ может переименовать фракцию в панели, но сейчас переименование откатывается при
|
||||||
|
каждом перезапуске приложения (задача #72).
|
||||||
|
|||||||
+24
-16
@@ -18,33 +18,41 @@ HTTPS твоими сертификатами и проксирует трафи
|
|||||||
└───────────────────────────────────────────────────┘
|
└───────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
- **PROD** — Pi. `docker compose up -d` поднимает два сервиса: `app` + `tunnel`. У `app`
|
- **PROD** — Pi. `docker compose up -d` поднимает три сервиса: `app` + `tunnel` + `backup`
|
||||||
**портов на хост нет** — наружу его выставляет только туннель-контейнер
|
(образы из Gitea-реестра, собираются на ПК `scripts/build-push.ps1`). У `app` **портов на
|
||||||
(`ssh -R 9000:app:8000` к VPS). Постоянно, Docker сам переподключает. См. [`pi/`](pi/README.md).
|
хост нет** — наружу его выставляет только туннель-контейнер (`ssh -R 9000:app:8000` к VPS).
|
||||||
- **TEST** — ПК. То же самое: `docker compose -f docker-compose.test.yml up` поднимает
|
Работает постоянно: при обрыве `ssh` завершается, и Docker перезапускает контейнер
|
||||||
`app` + `tunnel` (`ssh -R 9001:app:8000`). Портов на хост нет — тест виден только на
|
(`restart: unless-stopped`). См. [`pi/`](pi/README.md).
|
||||||
`forbidden-stars.ru`. Обычно запускается лаунчером при `APP_ENV=test`.
|
- **TEST** — ПК. То же самое: `docker compose -f docker-compose.test.yml up` собирает образы
|
||||||
|
локально и поднимает `app` + `tunnel` (`ssh -R 9001:app:8000`) + `backup` (без расписания и
|
||||||
|
без VPS). Портов на хост нет — тест виден только на `forbidden-stars.ru`. Обычно
|
||||||
|
запускается лаунчером при `APP_ENV=test`.
|
||||||
- **DEV** — ПК, нативно (`uvicorn`+`vite`). По умолчанию только на localhost; при
|
- **DEV** — ПК, нативно (`uvicorn`+`vite`). По умолчанию только на localhost; при
|
||||||
`LOCAL_PUBLIC=vps` лаунчер (`run.ps1`) дополнительно поднимает SSH-туннель с ПК
|
`LOCAL_PUBLIC=vps` лаунчер (`run.ps1` / `run.sh`) дополнительно поднимает SSH-туннель с ПК
|
||||||
(`ssh -R 9001:localhost:5173`) → дев виден на `forbidden-stars.ru`.
|
(`ssh -R 9001:localhost:5173`) → дев виден на `forbidden-stars.ru`.
|
||||||
- DEV и TEST делят слот **9001** (`forbidden-stars.ru`) → поднимай что-то **одно за раз**.
|
- DEV и TEST делят слот **9001** (`forbidden-stars.ru`) → поднимай что-то **одно за раз**.
|
||||||
PROD на отдельном слоте **9000** (`forbiddenstars.ru`) — работает независимо.
|
PROD на отдельном слоте **9000** (`forbiddenstars.ru`) — работает независимо.
|
||||||
|
Временный прод на ПК (`docker-compose.temp.yml`) тоже занимает **9000** — одновременно с Pi нельзя.
|
||||||
|
|
||||||
Ключ туннеля — **`deploy/tunnel/id_tunnel`** (приватный, в git не идёт). Его публичную
|
Ключи туннеля (приватные, в git не идут; публичные части — в `authorized_keys` пользователя
|
||||||
часть добавь в `authorized_keys` пользователя `tunnel` на VPS. Один и тот же ключ годится
|
`tunnel` на VPS):
|
||||||
для контейнерного туннеля (Pi/ПК) и для dev-туннеля `run.ps1`.
|
- **Pi** — `TUNNEL_KEY_B64` (base64 приватного ключа) в `.env`; файла ключа на Pi нет.
|
||||||
|
- **ПК, test и временный прод** — файл `deploy/tunnel/id_tunnel`, монтируется в туннель-контейнер.
|
||||||
|
- **ПК, dev** — `run.ps1`/`run.sh` зовут системный `ssh` без `-i`, то есть с ключом по
|
||||||
|
умолчанию из `~/.ssh`. Он должен быть в `authorized_keys` (можно тем же, что `id_tunnel`).
|
||||||
|
|
||||||
Настройка по шагам:
|
Настройка по шагам:
|
||||||
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): ключ в `deploy/tunnel/id_tunnel`, `.env`, `docker compose up -d`.
|
2. **Pi (прод)** — [`pi/README.md`](pi/README.md): ключ туннеля в `TUNNEL_KEY_B64`, `.env`, `docker compose up -d`.
|
||||||
3. **ПК (dev/test)** — тот же ключ в `deploy/tunnel/id_tunnel` (для test-контейнера) и/или
|
3. **ПК (dev/test)** — ключ в `deploy/tunnel/id_tunnel` (для test-контейнера) и/или ключ по
|
||||||
ключ по умолчанию для dev (`run.ps1`); pubkey — в `authorized_keys` у `tunnel@VPS`.
|
умолчанию в `~/.ssh` (для dev-туннеля); pubkey — в `authorized_keys` у `tunnel@VPS`.
|
||||||
4. **Бэкапы** — [`backup/README.md`](backup/README.md): контейнер `backup` (restic) делает
|
4. **Бэкапы** — [`backup/README.md`](backup/README.md): контейнер `backup` (restic) делает
|
||||||
снимки на Pi и на VPS (`fsbackup@VPS`, только SFTP), скрипты ПК скачивают их и проверяют
|
снимки на Pi и на VPS (`fsbackup@VPS`, только SFTP), скрипты ПК скачивают их и проверяют
|
||||||
восстановление на тест-клоне.
|
восстановление на тест-клоне.
|
||||||
|
|
||||||
Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, `id_tunnel*`) живут на VPS/Pi/ПК,
|
Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, `id_tunnel*`, `id_backup*`) живут на
|
||||||
в репозитории только `Caddyfile`, `deploy/tunnel/` (образ туннеля) и шаблоны.
|
VPS/Pi/ПК, в репозитории только `Caddyfile`, страница-заглушка, образы `deploy/tunnel/` и
|
||||||
|
`deploy/backup/` и шаблоны.
|
||||||
|
|
||||||
> Telegram-вход требует HTTPS-домен: у BotFather `/setdomain` укажи оба домена
|
> Telegram-вход требует HTTPS-домен: у BotFather `/setdomain` укажи оба домена
|
||||||
> (`forbiddenstars.ru` и `forbidden-stars.ru`).
|
> (`forbiddenstars.ru` и `forbidden-stars.ru`).
|
||||||
@@ -62,6 +70,6 @@ HTTPS твоими сертификатами и проксирует трафи
|
|||||||
внешний брокер (Redis pub/sub), иначе события увидит только тот воркер, что принял мутацию.
|
внешний брокер (Redis pub/sub), иначе события увидит только тот воркер, что принял мутацию.
|
||||||
|
|
||||||
SSE-поток сам не закрывается, поэтому uvicorn запускается с `--timeout-graceful-shutdown`
|
SSE-поток сам не закрывается, поэтому uvicorn запускается с `--timeout-graceful-shutdown`
|
||||||
(`entrypoint.sh`, `run.*`). Без него остановка ждёт закрытия всех соединений: в dev
|
(10 с в `entrypoint.sh`, 2 с в `run.*`). Без него остановка ждёт закрытия всех соединений: в dev
|
||||||
`--reload` при открытой вкладке висит вечно, в контейнере остановку обрывает только SIGKILL
|
`--reload` при открытой вкладке висит вечно, в контейнере остановку обрывает только SIGKILL
|
||||||
по `stop_grace_period`.
|
по `stop_grace_period`.
|
||||||
|
|||||||
+31
-14
@@ -55,8 +55,8 @@
|
|||||||
4. удаляет старые снимки по правилам хранения;
|
4. удаляет старые снимки по правилам хранения;
|
||||||
5. проверяет, что репозитории целы.
|
5. проверяет, что репозитории целы.
|
||||||
|
|
||||||
Раз в неделю (воскресенье, 05:30) дополнительно читает часть сохранённых данных и убеждается,
|
Раз в неделю (воскресенье, 05:30) дополнительно читает часть сохранённых данных (10 %) и
|
||||||
что последний снимок действительно восстанавливается.
|
убеждается, что БД из последнего снимка извлекается и проходит проверку целостности.
|
||||||
|
|
||||||
**Словарь**
|
**Словарь**
|
||||||
|
|
||||||
@@ -646,7 +646,7 @@
|
|||||||
| `Игроков` / `Партий` | сколько игроков и партий было в БД в этот момент — по ним легко найти «до поломки» |
|
| `Игроков` / `Партий` | сколько игроков и партий было в БД в этот момент — по ним легко найти «до поломки» |
|
||||||
| `Размер` | полный объём данных снимка |
|
| `Размер` | полный объём данных снимка |
|
||||||
| `Прирост` | сколько места снимок реально добавил в репозиторий |
|
| `Прирост` | сколько места снимок реально добавил в репозиторий |
|
||||||
| `Метки` | `scheduled` — по расписанию, `manual` — вручную, `pre-restore` — страховочный, прочие — имя, данное вручную |
|
| `Метки` | `scheduled` — по расписанию, `manual` — вручную, `pre-restore` — страховочный, `keep` + имя — именованный снимок (`run --tag имя`) |
|
||||||
|
|
||||||
Если локальный репозиторий повреждён или пуст, смотрите копию на VPS:
|
Если локальный репозиторий повреждён или пуст, смотрите копию на VPS:
|
||||||
`docker compose exec backup fs-backup list vps`.
|
`docker compose exec backup fs-backup list vps`.
|
||||||
@@ -737,30 +737,44 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
3. Дождитесь, пока приложение создаст пустую БД (`docker compose ps` → `app` `(healthy)`).
|
3. Дождитесь, пока приложение создаст пустую БД (`docker compose ps` → `app` `(healthy)`).
|
||||||
В журнале бэкапа это нормально (`docker compose logs backup`):
|
Контейнер `backup` стартует одновременно с `app` и сразу пробует сделать первый бэкап. В его
|
||||||
|
журнале (`docker compose logs backup`) нормально увидеть одно из двух:
|
||||||
|
|
||||||
```
|
- если `backup` успел раньше, чем `app` создал БД:
|
||||||
В БД нет ни игроков, ни партий, а последний снимок в репозитории vps — с данными.
|
|
||||||
Похоже на новый или очищенный сервер: бэкап НЕ сделан, чтобы пустые данные не вытеснили историю.
|
|
||||||
```
|
|
||||||
|
|
||||||
Это защита: пустой новый Pi не перезапишет историю на VPS.
|
```
|
||||||
|
ОШИБКА: БД /fs-db/forbidden_stars.db не найдена — приложение ещё ни разу не запускалось?
|
||||||
|
Первый бэкап не удался — следующая попытка по расписанию.
|
||||||
|
```
|
||||||
|
|
||||||
4. Посмотрите снимки на VPS и выберите нужный (обычно самый свежий):
|
- если БД уже была:
|
||||||
|
|
||||||
|
```
|
||||||
|
В БД нет ни игроков, ни партий, а последний снимок в репозитории vps — с данными.
|
||||||
|
Похоже на новый или очищенный сервер: бэкап НЕ сделан, чтобы пустые данные не вытеснили историю.
|
||||||
|
```
|
||||||
|
|
||||||
|
Это защита: пустой новый Pi не перезапишет историю на VPS.
|
||||||
|
|
||||||
|
4. Посмотрите снимки на VPS и выберите нужный (обычно самый свежий **с данными**):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec backup fs-backup list vps
|
docker compose exec backup fs-backup list vps
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Восстановите:
|
Проверьте, что у выбранного снимка в столбцах `Игроков`/`Партий` числа, а не `0` и не `?`.
|
||||||
|
Защита выше не распознаёт БД без таблиц: если первый бэкап попал ровно в момент создания
|
||||||
|
БД, в списке может появиться свежий снимок с `?` (задача #74). Такой снимок не выбирайте.
|
||||||
|
|
||||||
|
5. Восстановите, подставив ID из `list vps`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose stop app
|
docker compose stop app
|
||||||
docker compose exec backup fs-backup restore latest --repo vps --yes
|
docker compose exec backup fs-backup restore <ID> --repo vps --yes
|
||||||
docker compose start app
|
docker compose start app
|
||||||
```
|
```
|
||||||
|
|
||||||
Вместо `latest` можно указать ID из `list vps`.
|
Если самый свежий снимок в `list vps` — с данными, вместо ID можно написать `latest`.
|
||||||
|
|
||||||
6. Проверьте сайт. Затем сделайте снимок вручную, чтобы локальная копия появилась сразу:
|
6. Проверьте сайт. Затем сделайте снимок вручную, чтобы локальная копия появилась сразу:
|
||||||
|
|
||||||
@@ -852,7 +866,7 @@ docker compose logs --tail 100 backup
|
|||||||
| `снимок '…' не найден в репозитории` | опечатка в ID или снимок в другом репозитории | `fs-backup list` и `fs-backup list vps` |
|
| `снимок '…' не найден в репозитории` | опечатка в ID или снимок в другом репозитории | `fs-backup list` и `fs-backup list vps` |
|
||||||
| `service "backup" is not running` | контейнер не запущен | `docker compose up -d backup` |
|
| `service "backup" is not running` | контейнер не запущен | `docker compose up -d backup` |
|
||||||
| `permission denied while trying to connect to the Docker daemon socket` | пользователь Pi не в группе `docker` | `sudo usermod -aG docker $USER`, перезайти по SSH |
|
| `permission denied while trying to connect to the Docker daemon socket` | пользователь Pi не в группе `docker` | `sudo usermod -aG docker $USER`, перезайти по SSH |
|
||||||
| `docker compose ps` → `backup` `unhealthy` | последний успешный бэкап старше `BACKUP_MAX_AGE_HOURS` | `docker compose exec backup fs-backup health` покажет причину, дальше по таблице |
|
| `docker compose ps` → `backup` `unhealthy` | последний успешный бэкап старше `BACKUP_MAX_AGE_HOURS`, успешных бэкапов ещё не было или не задан `BACKUP_PASSWORD` | `docker compose exec backup fs-backup health` покажет причину, дальше по таблице |
|
||||||
|
|
||||||
**Про `recover`.** Команда сама определяет, на каком этапе оборвалось восстановление:
|
**Про `recover`.** Команда сама определяет, на каком этапе оборвалось восстановление:
|
||||||
- если замена успела пройти во всех томах — дочищает промежуточные папки, восстановленные
|
- если замена успела пройти во всех томах — дочищает промежуточные папки, восстановленные
|
||||||
@@ -907,8 +921,11 @@ docker volume rm <имя тома>
|
|||||||
| `export <ID\|latest> [--repo …]` | снимок в tar в stdout: `docker compose exec -T backup fs-backup export latest > fs.tar` |
|
| `export <ID\|latest> [--repo …]` | снимок в tar в stdout: `docker compose exec -T backup fs-backup export latest > fs.tar` |
|
||||||
| `restic <local\|vps> <аргументы>` | любая команда restic с настройками контейнера, например `restic vps stats` |
|
| `restic <local\|vps> <аргументы>` | любая команда restic с настройками контейнера, например `restic vps stats` |
|
||||||
| `help` | краткая справка |
|
| `help` | краткая справка |
|
||||||
|
| `init`, `info`, `health`, `has-snapshots` | служебные: создать репозитории, данные снимка для скриптов ПК, healthcheck, проверка «есть ли снимки» при старте |
|
||||||
|
|
||||||
Без `--yes` команды `restore` и `import` только показывают, что собираются сделать.
|
Без `--yes` команды `restore` и `import` только показывают, что собираются сделать.
|
||||||
|
Флаг `--no-pre-restore` у `restore`/`import` пропускает страховочный снимок — используйте,
|
||||||
|
только если текущие данные точно не нужны.
|
||||||
|
|
||||||
### Скрипт ПК
|
### Скрипт ПК
|
||||||
|
|
||||||
|
|||||||
+19
-6
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
На Pi нужны только **ДВА файла**: `docker-compose.yml` и `.env`. Репозиторий, сборка и файл
|
На Pi нужны только **ДВА файла**: `docker-compose.yml` и `.env`. Репозиторий, сборка и файл
|
||||||
ключа туннеля не нужны:
|
ключа туннеля не нужны:
|
||||||
- образы (`app` + `tunnel`) тянутся из Gitea-реестра (`pull_policy: always`);
|
- образы (`app` + `tunnel` + `backup`) тянутся из Gitea-реестра (`pull_policy: always`);
|
||||||
- приватный ключ туннеля лежит в `.env` как `TUNNEL_KEY_B64` (base64).
|
- приватный ключ туннеля лежит в `.env` как `TUNNEL_KEY_B64` (base64).
|
||||||
|
|
||||||
`docker compose up` поднимает два контейнера: `app` (FastAPI+SPA, портов на хост нет) и
|
`docker compose up` поднимает три контейнера: `app` (FastAPI+SPA, портов на хост нет),
|
||||||
`tunnel` (`ssh -R 9000:app:8000` к VPS). Публичная точка — VPS, домен `forbiddenstars.ru`
|
`tunnel` (`ssh -R 9000:app:8000` к VPS, стартует после `healthy` у `app`) и `backup` (restic,
|
||||||
(Pi за CGNAT — туннель стучится наружу сам).
|
см. раздел «Бэкапы»). Публичная точка — VPS, домен `forbiddenstars.ru` (Pi за CGNAT — туннель
|
||||||
|
стучится наружу сам).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,6 +87,14 @@ IMAGE_REGISTRY=gitea.arseniev.info/notbigghost # уже значение по
|
|||||||
IMAGE_TAG=latest
|
IMAGE_TAG=latest
|
||||||
```
|
```
|
||||||
`APP_ENV` (форсится в `production`) и `VPS_TUNNEL_PORT` (прод → 9000) не трогать.
|
`APP_ENV` (форсится в `production`) и `VPS_TUNNEL_PORT` (прод → 9000) не трогать.
|
||||||
|
Блок `BACKUP_*` можно оставить пустым: бэкапы включаются позже, по
|
||||||
|
[`deploy/backup/README.md`](../backup/README.md).
|
||||||
|
|
||||||
|
> В production приложение **не стартует**, если `SECRET_KEY` дефолтный или короче 32
|
||||||
|
> символов, а `ADMIN_PASSWORD` пустой или дефолтный (при `ADMIN_BOOTSTRAP_ENABLED=true`).
|
||||||
|
> `ADMIN_USERNAME`/`ADMIN_PASSWORD` применяются **только при первом создании** админа: если
|
||||||
|
> потом поменять их в `.env`, у существующего админа ничего не изменится, а штатной смены
|
||||||
|
> пароля админа на проде пока нет (задача #73).
|
||||||
|
|
||||||
## 4. Запуск
|
## 4. Запуск
|
||||||
```bash
|
```bash
|
||||||
@@ -94,7 +103,8 @@ docker compose up -d # pull_policy: always → тянет обр
|
|||||||
docker compose ps # app healthy → поднимется tunnel
|
docker compose ps # app healthy → поднимется tunnel
|
||||||
docker compose logs -f tunnel # ждём строку: [tunnel] -R 9000:app:8000 -> tunnel@...
|
docker compose logs -f tunnel # ждём строку: [tunnel] -R 9000:app:8000 -> tunnel@...
|
||||||
```
|
```
|
||||||
На старте контейнер сам применит миграции, засидит справочники и создаст админа из `.env`.
|
На старте контейнер сам применит миграции, засидит справочники и создаст админа из `.env`
|
||||||
|
(если админа ещё нет).
|
||||||
|
|
||||||
## 5. Проверка
|
## 5. Проверка
|
||||||
- Открой `https://forbiddenstars.ru` — должно отдать приложение (не заглушку).
|
- Открой `https://forbiddenstars.ru` — должно отдать приложение (не заглушку).
|
||||||
@@ -103,9 +113,12 @@ docker compose logs -f tunnel # ждём строку: [tunnel] -R 9000:a
|
|||||||
|
|
||||||
## Обновление
|
## Обновление
|
||||||
```bash
|
```bash
|
||||||
|
# Pi: docker compose exec backup fs-backup run --tag before-update # по желанию, если бэкапы включены
|
||||||
# ПК: .\scripts\build-push.ps1
|
# ПК: .\scripts\build-push.ps1
|
||||||
# Pi: docker compose up -d # always-pull подтянет свежий образ
|
# Pi: docker compose up -d # always-pull подтянет свежие образы
|
||||||
```
|
```
|
||||||
|
Если в новой версии менялся `docker-compose.yml` или `.env.example`, сначала скачайте
|
||||||
|
свежий `docker-compose.yml` (шаг 3) и допишите новые переменные в `.env`.
|
||||||
|
|
||||||
## Автозапуск после перезагрузки
|
## Автозапуск после перезагрузки
|
||||||
Уже обеспечен: `systemctl enable docker` + `restart: unless-stopped`. После `sudo reboot`
|
Уже обеспечен: `systemctl enable docker` + `restart: unless-stopped`. После `sudo reboot`
|
||||||
|
|||||||
+14
-9
@@ -4,8 +4,8 @@
|
|||||||
для двух доменов и проксирует трафик в SSH reverse-туннели от Pi (прод) и ПК (dev/test).
|
для двух доменов и проксирует трафик в SSH reverse-туннели от Pi (прод) и ПК (dev/test).
|
||||||
|
|
||||||
```
|
```
|
||||||
forbiddenstars.ru → 127.0.0.1:9000 ← Pi (autossh, постоянно) PROD
|
forbiddenstars.ru → 127.0.0.1:9000 ← Pi (туннель-контейнер, постоянно) PROD
|
||||||
forbidden-stars.ru → 127.0.0.1:9001 ← ПК (по требованию) DEV/TEST
|
forbidden-stars.ru → 127.0.0.1:9001 ← ПК (контейнер test или ssh dev, по требованию) DEV/TEST
|
||||||
```
|
```
|
||||||
|
|
||||||
> Туннель `ssh -R` по умолчанию слушает на loopback VPS (127.0.0.1) — ровно туда смотрит
|
> Туннель `ssh -R` по умолчанию слушает на loopback VPS (127.0.0.1) — ровно туда смотрит
|
||||||
@@ -51,7 +51,9 @@ 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
|
install -m 600 -o tunnel -g tunnel /dev/null /home/tunnel/.ssh/authorized_keys
|
||||||
```
|
```
|
||||||
Публичные ключи Pi и ПК добавишь в `/home/tunnel/.ssh/authorized_keys` на шагах настройки
|
Публичные ключи Pi и ПК добавишь в `/home/tunnel/.ssh/authorized_keys` на шагах настройки
|
||||||
Pi (`deploy/pi/README.md`) и ПК (через `ssh-copy-id`).
|
Pi (`deploy/pi/README.md`) и ПК (`deploy/tunnel/id_tunnel.pub` и/или ключ по умолчанию из
|
||||||
|
`~/.ssh` для dev-туннеля). Ключи **дописывай** (`>>`), а не перезаписывай файл (`>`):
|
||||||
|
иначе туннель другого хоста перестанет пускать.
|
||||||
|
|
||||||
## 5. Сертификаты
|
## 5. Сертификаты
|
||||||
|
|
||||||
@@ -67,11 +69,11 @@ Caddy читает **PEM** (текст с `-----BEGIN CERTIFICATE-----`). Рас
|
|||||||
|
|
||||||
Удобно собрать прямо на VPS — залей свои файлы и склей:
|
Удобно собрать прямо на VPS — залей свои файлы и склей:
|
||||||
```bash
|
```bash
|
||||||
mkdir -p /etc/caddy/certs/forbidden-stars.ru /root/certs-tmp
|
mkdir -p /etc/caddy/certs/forbidden-stars.ru /etc/caddy/certs/forbiddenstars.ru /root/certs-tmp
|
||||||
# с локальной машины (пример для домена forbidden-stars.ru):
|
# с локальной машины (пример для домена forbidden-stars.ru; имена файлов — свои):
|
||||||
scp forbidden-stars.crt intermediate.crt forbidden-stars.key root@186.246.51.17:/root/certs-tmp/
|
scp forbidden-stars.crt intermediate.crt forbidden-stars.key root@186.246.51.17:/root/certs-tmp/
|
||||||
# на VPS — собрать fullchain (leaf + промежуточный) и положить ключ:
|
# на 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 \
|
cat /root/certs-tmp/forbidden-stars.crt /root/certs-tmp/intermediate.crt \
|
||||||
> /etc/caddy/certs/forbidden-stars.ru/fullchain.pem
|
> /etc/caddy/certs/forbidden-stars.ru/fullchain.pem
|
||||||
cp /root/certs-tmp/forbidden-stars.key /etc/caddy/certs/forbidden-stars.ru/privkey.pem
|
cp /root/certs-tmp/forbidden-stars.key /etc/caddy/certs/forbidden-stars.ru/privkey.pem
|
||||||
# то же для forbiddenstars.ru (свои crt/intermediate/key), затем права
|
# то же для forbiddenstars.ru (свои crt/intermediate/key), затем права
|
||||||
@@ -96,13 +98,16 @@ scp deploy/vps/maintenance.html root@186.246.51.17:/etc/caddy/maintenance/mainte
|
|||||||
caddy validate --config /etc/caddy/Caddyfile
|
caddy validate --config /etc/caddy/Caddyfile
|
||||||
systemctl reload caddy
|
systemctl reload caddy
|
||||||
```
|
```
|
||||||
> Заглушка живёт в сниппете `(offline)` Caddyfile: при ответе апстрима 502/503/504
|
> Заглушка живёт в сниппете `(edge)` Caddyfile (там же security-заголовки и `no-store` для
|
||||||
> Caddy отдаёт `/etc/caddy/maintenance/maintenance.html` со статусом 503 и `Cache-Control: no-store`.
|
> HTML): при ответе апстрима 502/503/504 Caddy отдаёт `/etc/caddy/maintenance/maintenance.html`
|
||||||
|
> со статусом 503 и `Cache-Control: no-store`. Для `/api/events` (SSE) у каждого домена
|
||||||
|
> отдельный `handle` без `encode` и с `flush_interval -1`.
|
||||||
> Правки текста/вида — в `deploy/vps/maintenance.html`, затем повтори `scp` (reload Caddy не нужен,
|
> Правки текста/вида — в `deploy/vps/maintenance.html`, затем повтори `scp` (reload Caddy не нужен,
|
||||||
> файл читается на каждый запрос).
|
> файл читается на каждый запрос).
|
||||||
|
|
||||||
## 7. Проверка
|
## 7. Проверка
|
||||||
1. Подними туннель прода на Pi (см. `deploy/pi/README.md`) и/или dev/test на ПК (`run.ps1` при `LOCAL_PUBLIC=vps`).
|
1. Подними туннель прода на Pi (см. `deploy/pi/README.md`) и/или dev/test на ПК
|
||||||
|
(лаунчер `run.ps1`: dev — при `LOCAL_PUBLIC=vps`, test — при `APP_ENV=test`).
|
||||||
2. Открой `https://forbiddenstars.ru` и `https://forbidden-stars.ru`.
|
2. Открой `https://forbiddenstars.ru` и `https://forbidden-stars.ru`.
|
||||||
3. Пока соответствующий туннель не поднят — Caddy отдаёт страницу-заглушку «Технические шоколадки»
|
3. Пока соответствующий туннель не поднят — Caddy отдаёт страницу-заглушку «Технические шоколадки»
|
||||||
(HTTP 503), это ожидаемо.
|
(HTTP 503), это ожидаемо.
|
||||||
|
|||||||
Reference in New Issue
Block a user