diff --git a/.env.example b/.env.example index d97dfdd..f877d0c 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,14 @@ VPS_TUNNEL_USER=tunnel # Раскомментируй и переопредели, только если нужен нестандартный слот. #VPS_TUNNEL_PORT=9001 +# Приватный ключ туннеля в base64 — чтобы на Pi хватило только docker-compose.yml + .env +# (без файла deploy/tunnel/id_tunnel). Нужен ТОЛЬКО для прод-контейнера на Pi; для dev/test +# на ПК ключ берётся из файла. Сгенерируй ключ на ПК и закодируй БЕЗ переносов строк: +# Git Bash / Linux: base64 -w0 deploy/tunnel/id_tunnel +# PowerShell: [Convert]::ToBase64String([IO.File]::ReadAllBytes("deploy/tunnel/id_tunnel")) +# Pubkey (deploy/tunnel/id_tunnel.pub) добавь в authorized_keys у tunnel@VPS. +TUNNEL_KEY_B64= + # ─── АДМИНИСТРАТОР (вход по логину/паролю, отдельно от Telegram) ────────────── ADMIN_USERNAME=admin ADMIN_PASSWORD=change-me-admin-password diff --git a/deploy/pi/README.md b/deploy/pi/README.md index ab5a77b..7c9d0ee 100644 --- a/deploy/pi/README.md +++ b/deploy/pi/README.md @@ -1,89 +1,99 @@ # Raspberry Pi — ПРОД (`forbiddenstars.ru`) -Pi запускает приложение **и туннель** в Docker: `docker compose up` поднимает два сервиса — -`app` (FastAPI + SPA) и `tunnel` (ssh-контейнер). У `app` **портов на хост нет**, наружу он -доступен только через туннель-контейнер (`ssh -R 9000:app:8000` к VPS). Публичная точка — -VPS, домен `forbiddenstars.ru`. Снаружи Pi недоступен (CGNAT) — туннель сам стучится наружу. +На Pi нужны только **ДВА файла**: `docker-compose.yml` и `.env`. Репозиторий, сборка и файл +ключа туннеля не нужны: +- образы (`app` + `tunnel`) тянутся из Gitea-реестра (`pull_policy: always`); +- приватный ключ туннеля лежит в `.env` как `TUNNEL_KEY_B64` (base64). -## 1. Ключ для туннеля -Туннель-контейнер ходит на `tunnel@VPS` по ключу из `deploy/tunnel/id_tunnel` (gitignored). -Создай ключ и добавь его pubkey в `authorized_keys` пользователя `tunnel` на VPS: +`docker compose up` поднимает два контейнера: `app` (FastAPI+SPA, портов на хост нет) и +`tunnel` (`ssh -R 9000:app:8000` к VPS). Публичная точка — VPS, домен `forbiddenstars.ru` +(Pi за CGNAT, туннель стучится наружу сам). + +> Предпосылки: VPS настроен (Caddy `forbiddenstars.ru` → :9000, юзер `tunnel`) — см. +> `deploy/vps/README.md`. + +--- + +## A. На ПК (один раз + при каждом релизе) + +**Ключ туннеля и его base64 для `.env`** (один раз): ```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) +ssh-copy-id -i deploy/tunnel/id_tunnel.pub tunnel@186.246.51.17 # pubkey → authorized_keys на VPS +# base64 приватного ключа БЕЗ переносов строк (значение для TUNNEL_KEY_B64 в .env): +# Git Bash / Linux: base64 -w0 deploy/tunnel/id_tunnel +# PowerShell: [Convert]::ToBase64String([IO.File]::ReadAllBytes("deploy/tunnel/id_tunnel")) ``` -## 2. .env -Все настройки приложения и туннеля — в одном `.env` (контейнер форсит `APP_ENV=production`): +**Сборка и пуш образов под arm64** (при каждом релизе): +```powershell +docker login gitea.arseniev.info +.\scripts\build-push.ps1 # *nix: bash scripts/build-push.sh +``` + +## B. На Pi + +**1. Docker (один раз):** ```bash -cp .env.example .env +sudo apt update && sudo apt -y full-upgrade +curl -fsSL https://get.docker.com | sudo sh +sudo usermod -aG docker $USER +sudo systemctl enable --now docker # автозапуск после ребута +newgrp docker # применить группу (или перезайти по SSH) ``` -Минимально заполнить на Pi: + +**2. Положить два файла** (любой каталог, напр. `~/forbidden-stars`): +```bash +mkdir -p ~/forbidden-stars && cd ~/forbidden-stars +# docker-compose.yml из Gitea (raw); branch — main (или dev, если ещё не смёржено): +curl -fsSLO https://gitea.arseniev.info/NotBigGhost/ForbiddenStarsApp/raw/branch/main/docker-compose.yml +# .env: можно взять шаблон оттуда же и заполнить +curl -fsSL https://gitea.arseniev.info/NotBigGhost/ForbiddenStarsApp/raw/branch/main/.env.example -o .env +nano .env ``` -SECRET_KEY=... # python -c "import secrets;print(secrets.token_urlsafe(48))" +Заполнить в `.env`: +``` +SECRET_KEY=... # python3 -c "import secrets;print(secrets.token_urlsafe(48))" ADMIN_USERNAME=... # логин/пароль секретной админки ADMIN_PASSWORD=... TELEGRAM_BOT_TOKEN=... # бот @BotFather; /setdomain → forbiddenstars.ru TELEGRAM_BOT_USERNAME=... -VPS_TUNNEL_HOST=186.246.51.17 # читает сервис tunnel -VPS_TUNNEL_USER=tunnel +VPS_TUNNEL_HOST=186.246.51.17 +TUNNEL_KEY_B64=... # base64 ключа из шага A (одной строкой) +IMAGE_REGISTRY=gitea.arseniev.info/notbigghost # уже значение по умолчанию +IMAGE_TAG=latest ``` -`VPS_TUNNEL_PORT` задавать не нужно — прод-контейнер по умолчанию берёт слот **9000**. -Ресурсы (`APP_MEM_LIMIT`/`APP_CPUS`) и оффсайт-бэкап (`BACKUP_*`) — по желанию. +`APP_ENV` менять не нужно (контейнер форсит `production`); `VPS_TUNNEL_PORT` — тоже (прод → 9000). -## 3. Запуск (образ из реестра) -Образы (`app` + `tunnel`) собираются под arm64 на ПК и пушатся в Gitea-реестр, Pi их тянет -(см. `IMAGE_REGISTRY`/`IMAGE_TAG` в `.env`). Pi 4/5 — мощный, но реестр держим как основной путь. - -На ПК (Docker Desktop) — один раз `docker login gitea.arseniev.info`, далее при каждом релизе: -```powershell -.\scripts\build-push.ps1 # Windows (PowerShell); на *nix — bash scripts/build-push.sh -``` -> `bash scripts/build-push.sh` в PowerShell уходит в WSL и падает, если нет дистрибутива — -> используй `.ps1`. Команды внутри — обычный `docker buildx`, работают и напрямую в PowerShell. -На Pi: +**3. Запуск:** ```bash -docker login gitea.arseniev.info # один раз (доступ к пакету) -git pull # нужны docker-compose.yml, .env, deploy/tunnel/id_tunnel -docker compose pull # тянет свежие образы из реестра -docker compose up -d +docker login gitea.arseniev.info # один раз (доступ к реестру) +docker compose up -d # pull_policy: always → сам тянет образы, без сборки docker compose ps # app healthy → поднимется tunnel docker compose logs -f tunnel # ждём: [tunnel] -R 9000:app:8000 -> tunnel@... ``` -Открой `https://forbiddenstars.ru` — должен отдать прод. - -> Альтернатива без реестра (Pi 4/5 ≥4ГБ потянет сборку): `docker compose up -d --build` прямо на Pi. -> Реестр Gitea должен быть по HTTPS с валидным сертификатом; иначе на Pi добавь его в -> `/etc/docker/daemon.json` → `insecure-registries`. +На старте контейнер сам применит миграции, засидит справочники и создаст админа из `.env`. +Открой `https://forbiddenstars.ru`. У @BotFather `/setdomain` → добавь `forbiddenstars.ru`. ## Обновление ```bash -# ПК: scripts/build-push.sh -# Pi: git pull && docker compose pull && docker compose up -d +# ПК: .\scripts\build-push.ps1 +# Pi: docker compose up -d # always-pull подтянет свежий образ ``` -## Бэкап (полный + оффсайт на VPS) -`scripts/backup.sh` делает консистентный онлайн-снимок и собирает **полный** архив -`fs_.tar.gz` (БД + `uploads/` + `achievements/`) в `backups/` с ротацией. Если в `.env` -задан `BACKUP_VPS_HOST` — копия дополнительно уходит на VPS (оффсайт; Pi → VPS push, т.к. CGNAT). +## Автозапуск после перезагрузки +Обеспечен: `systemctl enable docker` + `restart: unless-stopped`. После `sudo reboot` +контейнеры поднимаются сами (используют локальный образ, без повторного pull). -Настройка оффсайта (один раз): -```bash -# 1) отдельный ключ бэкапа + его pubkey на VPS (юзер backup — см. deploy/vps/README.md): -ssh-keygen -t ed25519 -f deploy/backup/id_backup -N "" -ssh-copy-id -i deploy/backup/id_backup.pub backup@186.246.51.17 -# 2) в .env заполнить BACKUP_VPS_HOST=186.246.51.17 (по желанию — BACKUP_VPS_USER/DIR/KEEP_*) -# 3) разовый прогон и проверка, что архив доехал на VPS: -scripts/backup.sh -# 4) cron (ежедневно в 04:00): -crontab -e -# 0 4 * * * /home/pi/ForbidenStarsApp/scripts/backup.sh >> /var/log/fs-backup.log 2>&1 -``` +## Бэкап (опционально) +Бэкап-скриптам нужен доступ к Docker и к самому `docker-compose.yml` (он уже на Pi). Скопируй +рядом ещё `scripts/backup.sh` и `scripts/restore.sh` (или склонируй репозиторий для бэкапов). +Затем — отдельный ключ `backup@VPS`, `BACKUP_VPS_HOST` в `.env`, cron. Подробнее — комментарии +в `scripts/backup.sh` и `deploy/vps/README.md` §8. Восстановление: `scripts/restore.sh <архив>`. -Восстановление: `scripts/restore.sh backups/fs_.tar.gz` (ДЕСТРУКТИВНО — заменяет текущие -данные; сначала проверь на тест-контуре). Тома — `db-data` / `uploads-data` / `achievements-data`. - -Логи: `docker compose logs -f app` / `... tunnel`. Если `https://forbiddenstars.ru` отдаёт -502 — значит туннель не поднят: смотри `docker compose logs tunnel` (чаще всего ключ не в -`authorized_keys` на VPS или не заданы `VPS_TUNNEL_*` в `.env`). +## Если что-то не так +- `https://forbiddenstars.ru` отдаёт заглушку/502 → туннель не поднят: `docker compose logs tunnel` + (чаще: pubkey не в `authorized_keys` на VPS, пустой `TUNNEL_KEY_B64`, или на VPS занят слот 9000 + → `sudo fuser -k 9000/tcp` на VPS, затем `docker compose restart tunnel`). +- `pull` не проходит → проверь `docker login gitea.arseniev.info` и что реестр по HTTPS с валидным + сертификатом (иначе хост в `/etc/docker/daemon.json` → `insecure-registries`, `systemctl restart docker`). diff --git a/deploy/tunnel/tunnel.sh b/deploy/tunnel/tunnel.sh index 9ec8628..78df777 100644 --- a/deploy/tunnel/tunnel.sh +++ b/deploy/tunnel/tunnel.sh @@ -9,18 +9,20 @@ 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)." +# Источник приватного ключа: либо TUNNEL_KEY_B64 (base64 в .env — прод: только compose+env), +# либо смонтированный файл /key/id_tunnel (dev/test, где репозиторий есть на хосте). +mkdir -p /root/.ssh +KEY=/root/.ssh/id_tunnel +if [ -n "${TUNNEL_KEY_B64:-}" ]; then + echo "$TUNNEL_KEY_B64" | base64 -d > "$KEY" +elif [ -f /key/id_tunnel ]; then + cp /key/id_tunnel "$KEY" +else + echo "[tunnel] ERROR: нет ключа — задай TUNNEL_KEY_B64 в .env (base64) или примонтируй /key/id_tunnel." + echo "[tunnel] base64 ключа без переносов: base64 -w0 deploy/tunnel/id_tunnel" exit 1 fi - -# Копируем ключ и выставляем строгие права (с хоста права могут не годиться, особенно с Windows). -mkdir -p /root/.ssh -cp /key/id_tunnel /root/.ssh/id_tunnel -chmod 600 /root/.ssh/id_tunnel +chmod 600 "$KEY" echo "[tunnel] -R ${VPS_TUNNEL_PORT}:${UPSTREAM} -> ${VPS_TUNNEL_USER}@${VPS_TUNNEL_HOST}" exec ssh -N \ diff --git a/docker-compose.yml b/docker-compose.yml index 21800e9..b0a272a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,11 @@ # ───────────────────────────────────────────────────────────────────────────── # ПРОД на Raspberry Pi (forbiddenstars.ru) — единый compose: приложение + туннель. -# Все настройки и секреты — в корневом .env (см. .env.example). Этот файл их только -# пробрасывает; ничего секретного тут нет. +# На Pi нужны только ДВА файла: этот docker-compose.yml и .env. Репозиторий, сборка и +# файл ключа не нужны: образы тянутся из реестра (pull_policy: always), а приватный ключ +# туннеля лежит в .env (TUNNEL_KEY_B64, base64). build: оставлен для сборки/пуша на ПК +# (scripts/build-push.*) — на Pi он не используется. # -# Запуск/обновление: docker compose up -d --build +# Запуск/обновление: docker compose up -d (сам тянет свежие образы) # Логи: docker compose logs -f app (или: ... tunnel) # Остановка: docker compose down (данные в томах сохраняются) # Бэкап/restore: scripts/backup.sh / scripts/restore.sh @@ -18,6 +20,7 @@ services: # Образ из реестра (Gitea): собирается под arm64 на ПК (scripts/build-push.sh) и тянется # на Pi через `docker compose pull`. build: оставлен как локальный фолбэк (сборка на Pi). image: ${IMAGE_REGISTRY:-gitea.arseniev.info/notbigghost}/forbidden-stars:${IMAGE_TAG:-latest} + pull_policy: always # на Pi всегда тянем образ из реестра (без сборки) restart: unless-stopped init: true # PID 1 пробрасывает сигналы → корректный SIGTERM stop_grace_period: 30s # время на чек-пойнт WAL SQLite перед остановкой @@ -46,22 +49,22 @@ services: max-file: "5" # SSH reverse-туннель: VPS:${VPS_TUNNEL_PORT:-9000} → app:8000 (домен forbiddenstars.ru). - # Ключ — ./deploy/tunnel/id_tunnel (в git не идёт); его pubkey в authorized_keys у tunnel@VPS. + # Приватный ключ — в .env как TUNNEL_KEY_B64 (base64); pubkey — в authorized_keys у tunnel@VPS. tunnel: build: ./deploy/tunnel image: ${IMAGE_REGISTRY:-gitea.arseniev.info/notbigghost}/forbidden-stars-tunnel:${IMAGE_TAG:-latest} + pull_policy: always restart: unless-stopped init: true depends_on: app: condition: service_healthy environment: - VPS_TUNNEL_HOST: ${VPS_TUNNEL_HOST:?VPS_TUNNEL_HOST is required in .env} + VPS_TUNNEL_HOST: ${VPS_TUNNEL_HOST} # обязателен; tunnel.sh упадёт с ошибкой, если пуст VPS_TUNNEL_USER: ${VPS_TUNNEL_USER:-tunnel} VPS_TUNNEL_PORT: ${VPS_TUNNEL_PORT:-9000} # прод-слот (forbiddenstars.ru) UPSTREAM: app:8000 - volumes: - - ./deploy/tunnel/id_tunnel:/key/id_tunnel:ro + TUNNEL_KEY_B64: ${TUNNEL_KEY_B64:-} # приватный ключ туннеля (base64), из .env mem_limit: 64m security_opt: - no-new-privileges:true