From c544f720631504055cde0d11c2d755f9dcafeba8 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Sat, 27 Jun 2026 11:47:28 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=92=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=BF=D1=80=D0=BE=D0=B4=20=D0=BD=D0=B0=20=D0=9F?= =?UTF-8?q?=D0=9A:=20docker-compose.temp.yml=20(forbiddenstars.ru/9000,=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5=20produc?= =?UTF-8?q?tion)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Заглушка вместо Pi, пока он не готов: поднимает forbiddenstars.ru с ПК через слот VPS 9000, APP_ENV=production, локальная x86-сборка (без реестра), отдельный проект forbidden-stars-temp со своими постоянными томами под прод-данные. Позже данные мигрируют на Pi (backup → restore). Co-Authored-By: Claude Opus 4.8 --- docker-compose.temp.yml | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docker-compose.temp.yml diff --git a/docker-compose.temp.yml b/docker-compose.temp.yml new file mode 100644 index 0000000..3baaf72 --- /dev/null +++ b/docker-compose.temp.yml @@ -0,0 +1,74 @@ +# ───────────────────────────────────────────────────────────────────────────── +# ВРЕМЕННЫЙ ПРОД НА ПК — заглушка вместо Pi, пока он не готов. +# Поднимает forbiddenstars.ru (слот VPS 9000) с твоего ПК: поведение production, +# прод-данные в ПОСТОЯННЫХ томах (потом перенесёшь на Pi через backup/restore). +# +# Отличия от docker-compose.yml (прод на Pi): +# • локальный образ (сборка x86 на ПК), НЕ из реестра и НЕ пушится; +# • отдельный проект (name) и свои тома — не конфликтует с dev/test на этом ПК. +# Всё остальное — как у прода (APP_ENV=production, туннель на 9000, лимиты, healthcheck). +# +# Запуск: docker compose -f docker-compose.temp.yml up -d --build +# Логи: docker compose -f docker-compose.temp.yml logs -f tunnel +# Остановка: docker compose -f docker-compose.temp.yml down (тома сохраняются) +# +# ВАЖНО: слот 9000 общий с будущим прод-туннелем Pi. Перед запуском прода на Pi +# ОСТАНОВИ этот контур (down), иначе оба будут драться за VPS:9000. +# ───────────────────────────────────────────────────────────────────────────── +name: forbidden-stars-temp + +services: + app: + build: . + image: forbidden-stars:temp-prod # локальный образ x86 (на ПК); в реестр НЕ пушим + restart: unless-stopped + init: true + stop_grace_period: 30s + env_file: + - .env + environment: + APP_ENV: production # ведём себя ровно как прод + volumes: + - db-data:/data + - uploads-data:/data/uploads + - achievements-data:/data/achievements + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 40s + mem_limit: ${APP_MEM_LIMIT:-1g} # на ПК ресурсов больше, чем на Pi + cpus: ${APP_CPUS:-2} + security_opt: + - no-new-privileges:true + logging: + driver: json-file + options: + max-size: "10m" + max-file: "5" + + # Туннель на ПРОД-слот: VPS:${VPS_TUNNEL_PORT:-9000} → app:8000 (домен forbiddenstars.ru). + tunnel: + build: ./deploy/tunnel + image: forbidden-stars-tunnel:temp-prod + 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_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 + mem_limit: 64m + security_opt: + - no-new-privileges:true + +volumes: + db-data: + uploads-data: + achievements-data: From 029a043bfc40c0bafba9eca7f132d81b98879bc6 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Mon, 29 Jun 2026 21:48:40 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B4=20=D0=BD=D0=B0=20P?= =?UTF-8?q?i:=20=D0=B4=D0=B5=D0=BF=D0=BB=D0=BE=D0=B9=20=D0=B8=D0=B7=202=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20(compose=20+=20.env),=20?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=20=D1=82=D1=83=D0=BD=D0=BD=D0=B5=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B8=D0=B7=20.env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Теперь на Pi нужны только docker-compose.yml и .env — без репозитория, сборки и файла ключа. - docker-compose.yml: pull_policy: always (образы тянутся из реестра, сборка не запускается); ключ туннеля больше не монтируется томом — берётся из .env (TUNNEL_KEY_B64, base64); у VPS_TUNNEL_HOST убран :? (ломал бы buildx bake на ПК, где переменная не задана) — проверка обязательности перенесена в tunnel.sh (рантайм). - deploy/tunnel/tunnel.sh: приватный ключ из TUNNEL_KEY_B64 (base64) с фолбэком на смонтированный /key/id_tunnel (для dev/test, где репозиторий на хосте). - .env.example: TUNNEL_KEY_B64 + как сгенерировать base64 (Git Bash / PowerShell). - deploy/pi/README: полный поток «голый Pi → 2 файла»: Docker, curl compose+.env из Gitea, up; обновление, автозапуск, бэкап как опция. Co-Authored-By: Claude Opus 4.8 --- .env.example | 8 +++ deploy/pi/README.md | 134 +++++++++++++++++++++------------------- deploy/tunnel/tunnel.sh | 22 ++++--- docker-compose.yml | 17 ++--- 4 files changed, 102 insertions(+), 79 deletions(-) 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 From d81f5d8964eb28adf8ee12af0b026c846579a5ca Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Mon, 29 Jun 2026 22:00:10 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=94=D0=BE=D0=BA=D0=B8=20Pi:=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D0=B0=D1=8F=20=D0=B8=D0=BD=D1=81=D1=82=D1=80?= =?UTF-8?q?=D1=83=D0=BA=D1=86=D0=B8=D1=8F=20=D1=81=20=D0=BD=D1=83=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=B4=20=D0=B4=D0=B5=D0=BF=D0=BB=D0=BE?= =?UTF-8?q?=D0=B9=20=D0=B8=D0=B7=202=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE?= =?UTF-8?q?=D0=B2;=20=D0=BA=D0=BB=D1=8E=D1=87=20=D1=82=D1=83=D0=BD=D0=BD?= =?UTF-8?q?=D0=B5=D0=BB=D1=8F=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B8=D1=80?= =?UTF-8?q?=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D0=BD=D0=B0=20Pi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy/pi/README: пошагово от чистого Pi (Docker → SSH-ключ туннеля на самом Pi → 2 файла → up), с объяснением назначения ключа и кодирования в base64 (TUNNEL_KEY_B64). - .env.example/README: PowerShell-команда base64 через Resolve-Path (абсолютный путь — иначе [IO.File]::ReadAllBytes искал относительно домашнего каталога .NET и падал). Co-Authored-By: Claude Opus 4.8 --- .env.example | 2 +- deploy/pi/README.md | 117 +++++++++++++++++++++++++++----------------- 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/.env.example b/.env.example index f877d0c..82f46cf 100644 --- a/.env.example +++ b/.env.example @@ -31,7 +31,7 @@ VPS_TUNNEL_USER=tunnel # (без файла 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")) +# PowerShell: [Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path "deploy/tunnel/id_tunnel"))) # Pubkey (deploy/tunnel/id_tunnel.pub) добавь в authorized_keys у tunnel@VPS. TUNNEL_KEY_B64= diff --git a/deploy/pi/README.md b/deploy/pi/README.md index 7c9d0ee..24cd037 100644 --- a/deploy/pi/README.md +++ b/deploy/pi/README.md @@ -7,48 +7,70 @@ `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`. +(Pi за CGNAT — туннель стучится наружу сам). --- -## A. На ПК (один раз + при каждом релизе) +## 0. Предпосылки (один раз, не на Pi) +1. **VPS настроен**: Caddy `forbiddenstars.ru` → `127.0.0.1:9000`, пользователь `tunnel`, + сертификаты, страница-заглушка — см. `deploy/vps/README.md`. +2. **Образы собраны и запушены в реестр** (на ПК с Docker Desktop): + ```powershell + docker login gitea.arseniev.info + .\scripts\build-push.ps1 + ``` + Без этого `docker compose up` на Pi не найдёт образы в реестре. -**Ключ туннеля и его 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 # 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")) -``` - -**Сборка и пуш образов под arm64** (при каждом релизе): -```powershell -docker login gitea.arseniev.info -.\scripts\build-push.ps1 # *nix: bash scripts/build-push.sh -``` - -## B. На Pi - -**1. Docker (один раз):** +## 1. Система и Docker на Pi ```bash +ssh pi@ sudo apt update && sudo apt -y full-upgrade -curl -fsSL https://get.docker.com | sudo sh +sudo apt -y install curl openssh-client # base64/ssh-keygen уже есть в системе + +curl -fsSL https://get.docker.com | sudo sh # Docker Engine + compose-плагин (arm64) sudo usermod -aG docker $USER -sudo systemctl enable --now docker # автозапуск после ребута -newgrp docker # применить группу (или перезайти по SSH) +sudo systemctl enable --now docker # автозапуск после ребута +newgrp docker # применить группу (или перезайти по SSH) +docker version && docker compose version # проверка ``` -**2. Положить два файла** (любой каталог, напр. `~/forbidden-stars`): +## 2. SSH-ключ для туннеля +**Что это.** Отдельная пара ключей **только для туннеля** — ею контейнер `tunnel` логинится на +`tunnel@VPS`, чтобы открыть `ssh -R`. Это не системный ключ Pi, ты создаёшь его сам. Распределение: +- **приватный** ключ → в `.env` как `TUNNEL_KEY_B64` (base64, одной строкой); +- **публичный** (`.pub`) → в `authorized_keys` пользователя `tunnel` на VPS. + +Сгенерировать прямо на Pi: +```bash +ssh-keygen -t ed25519 -f ~/fs_tunnel -N "" # создаст ~/fs_tunnel (приватный) и ~/fs_tunnel.pub +``` +Добавить публичный ключ на VPS (Pi ходит наружу — это работает даже за CGNAT): +```bash +ssh-copy-id -i ~/fs_tunnel.pub tunnel@186.246.51.17 +# Если у tunnel нет пароля (только ключ) — добавь вручную через свой админ-доступ к VPS: +# cat ~/fs_tunnel.pub # скопируй строку +# на VPS: echo '<строка>' >> /home/tunnel/.ssh/authorized_keys +``` +Закодировать приватный ключ в base64 **одной строкой** — это значение для `TUNNEL_KEY_B64`: +```bash +base64 -w0 ~/fs_tunnel; echo # выведет длинную строку без переносов — скопируй её целиком +``` +> Альтернатива: если ключ уже есть на ПК (`deploy/tunnel/id_tunnel`) и его pubkey уже на VPS — +> не плоди новый, закодируй тот: +> `[Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path "deploy\tunnel\id_tunnel")))` + +После того как base64 вставлен в `.env`, файлы ключа на Pi можно удалить — ключ теперь в `.env`, +а pubkey уже на VPS: +```bash +shred -u ~/fs_tunnel ~/fs_tunnel.pub # или просто rm +``` + +## 3. Два файла: docker-compose.yml + .env ```bash mkdir -p ~/forbidden-stars && cd ~/forbidden-stars -# docker-compose.yml из Gitea (raw); branch — main (или dev, если ещё не смёржено): +# branch — main (или dev, если ещё не смёржено в main): 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 +curl -fsSL https://gitea.arseniev.info/NotBigGhost/ForbiddenStarsApp/raw/branch/main/.env.example -o .env nano .env ``` Заполнить в `.env`: @@ -56,24 +78,28 @@ nano .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_TOKEN=... # бот @BotFather; затем /setdomain → forbiddenstars.ru TELEGRAM_BOT_USERNAME=... VPS_TUNNEL_HOST=186.246.51.17 -TUNNEL_KEY_B64=... # base64 ключа из шага A (одной строкой) +TUNNEL_KEY_B64=... # длинная строка base64 из шага 2 (целиком, одной строкой) IMAGE_REGISTRY=gitea.arseniev.info/notbigghost # уже значение по умолчанию IMAGE_TAG=latest ``` -`APP_ENV` менять не нужно (контейнер форсит `production`); `VPS_TUNNEL_PORT` — тоже (прод → 9000). +`APP_ENV` (форсится в `production`) и `VPS_TUNNEL_PORT` (прод → 9000) не трогать. -**3. Запуск:** +## 4. Запуск ```bash -docker login gitea.arseniev.info # один раз (доступ к реестру) -docker compose up -d # pull_policy: always → сам тянет образы, без сборки +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@... +docker compose logs -f tunnel # ждём строку: [tunnel] -R 9000:app:8000 -> tunnel@... ``` На старте контейнер сам применит миграции, засидит справочники и создаст админа из `.env`. -Открой `https://forbiddenstars.ru`. У @BotFather `/setdomain` → добавь `forbiddenstars.ru`. + +## 5. Проверка +- Открой `https://forbiddenstars.ru` — должно отдать приложение (не заглушку). +- У @BotFather `/setdomain` → добавь `forbiddenstars.ru` (иначе Telegram-вход не заработает). +- Админка: удержать «Меню» 10 с → `/admin/login`, войти логином/паролём из `.env`. ## Обновление ```bash @@ -82,18 +108,17 @@ docker compose logs -f tunnel # ждём: [tunnel] -R 9000:app:8000 -> tu ``` ## Автозапуск после перезагрузки -Обеспечен: `systemctl enable docker` + `restart: unless-stopped`. После `sudo reboot` -контейнеры поднимаются сами (используют локальный образ, без повторного pull). +Уже обеспечен: `systemctl enable docker` + `restart: unless-stopped`. После `sudo reboot` +контейнеры поднимутся сами (используют локальный образ, без повторного pull). ## Бэкап (опционально) -Бэкап-скриптам нужен доступ к 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 <архив>`. +Бэкап-скриптам нужен сам `docker-compose.yml` (он на Pi) + скрипты. Скопируй рядом +`scripts/backup.sh` и `scripts/restore.sh`, настрой `BACKUP_*` в `.env` и cron. Подробно — +комментарии в `scripts/backup.sh` и `deploy/vps/README.md` §8. ## Если что-то не так - `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`). + (чаще: pubkey не в `authorized_keys` на VPS, пустой/битый `TUNNEL_KEY_B64`, либо на VPS занят + слот 9000 → на VPS `sudo fuser -k 9000/tcp`, затем `docker compose restart tunnel`). - `pull` не проходит → проверь `docker login gitea.arseniev.info` и что реестр по HTTPS с валидным сертификатом (иначе хост в `/etc/docker/daemon.json` → `insecure-registries`, `systemctl restart docker`).