v1.0 - Финальная проверка изоляции продакшна. Настройка бэкапа на VPS, подготовка образа
This commit is contained in:
+34
-2
@@ -22,11 +22,15 @@ LOCAL_PUBLIC=local
|
|||||||
# Ключ туннеля — в deploy/tunnel/id_tunnel (в git не идёт); pubkey → authorized_keys у tunnel@VPS.
|
# Ключ туннеля — в 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 для dev-туннеля (forbidden-stars.ru); контейнеры: prod=9000, test=9001
|
# VPS_TUNNEL_PORT обычно НЕ задают — каждый контур берёт свой слот по умолчанию:
|
||||||
|
# dev (run.ps1) → 9001, прод-контейнер → 9000, test-контейнер → 9001.
|
||||||
|
# Раскомментируй и переопредели, только если нужен нестандартный слот.
|
||||||
|
#VPS_TUNNEL_PORT=9001
|
||||||
|
|
||||||
# ─── АДМИНИСТРАТОР (вход по логину/паролю, отдельно от Telegram) ──────────────
|
# ─── АДМИНИСТРАТОР (вход по логину/паролю, отдельно от Telegram) ──────────────
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
ADMIN_PASSWORD=change-me-admin-password
|
ADMIN_PASSWORD=change-me-admin-password
|
||||||
|
ADMIN_NICKNAME=Администратор
|
||||||
ADMIN_BOOTSTRAP_ENABLED=true
|
ADMIN_BOOTSTRAP_ENABLED=true
|
||||||
|
|
||||||
# ─── АУТЕНТИФИКАЦИЯ ИГРОКОВ ───────────────────────────────────────────────────
|
# ─── АУТЕНТИФИКАЦИЯ ИГРОКОВ ───────────────────────────────────────────────────
|
||||||
@@ -57,10 +61,38 @@ COOKIE_DOMAIN=
|
|||||||
DEV_DATABASE_URL=sqlite:///./data/dev/forbidden_stars.db
|
DEV_DATABASE_URL=sqlite:///./data/dev/forbidden_stars.db
|
||||||
PROD_DATABASE_URL=sqlite:////data/forbidden_stars.db
|
PROD_DATABASE_URL=sqlite:////data/forbidden_stars.db
|
||||||
|
|
||||||
# Каталог загрузок (зарезервировано под фото/видео), тоже раздельно.
|
# Каталог загрузок (фото партий), раздельно для dev и prod.
|
||||||
DEV_UPLOAD_DIR=./data/dev/uploads
|
DEV_UPLOAD_DIR=./data/dev/uploads
|
||||||
PROD_UPLOAD_DIR=/data/uploads
|
PROD_UPLOAD_DIR=/data/uploads
|
||||||
|
|
||||||
|
# Каталог определений титулов (файлы: meta.json, condition.py, icon.*), раздельно.
|
||||||
|
DEV_ACHIEVEMENTS_DIR=./data/dev/achievements
|
||||||
|
PROD_ACHIEVEMENTS_DIR=/data/achievements
|
||||||
|
|
||||||
|
# ─── ОБРАЗЫ ПРОДА (реестр для docker compose pull на Pi) ──────────────────────
|
||||||
|
# Образы собираются под arm64 на ПК (scripts/build-push.sh) и пушатся в Gitea-реестр,
|
||||||
|
# а Pi их тянет (docker compose pull). Owner в пути — строчными. Тег можно версионировать.
|
||||||
|
# Перед пушем/пуллом: docker login gitea.arseniev.info
|
||||||
|
IMAGE_REGISTRY=gitea.arseniev.info/notbigghost
|
||||||
|
IMAGE_TAG=latest
|
||||||
|
|
||||||
|
# ─── РЕСУРСЫ ПРОД-КОНТЕЙНЕРА (docker-compose.yml) ─────────────────────────────
|
||||||
|
# Лимиты под Raspberry Pi. Не заданы → дефолты compose (512m / 1.5 CPU).
|
||||||
|
# Подними, если у Pi больше RAM/ядер.
|
||||||
|
#APP_MEM_LIMIT=512m
|
||||||
|
#APP_CPUS=1.5
|
||||||
|
|
||||||
|
# ─── БЭКАП (оффсайт-копия на VPS; читает scripts/backup.sh на хосте Pi) ───────
|
||||||
|
# Полный бэкап (БД + uploads + achievements) кладётся локально в backups/, и — если задан
|
||||||
|
# BACKUP_VPS_HOST — копируется на VPS (Pi → VPS push, т.к. Pi за CGNAT). Пусто = только локально.
|
||||||
|
# Ключ бэкапа — deploy/backup/id_backup (в git НЕ идёт); его pubkey → authorized_keys у backup@VPS.
|
||||||
|
BACKUP_VPS_HOST=
|
||||||
|
BACKUP_VPS_USER=backup
|
||||||
|
BACKUP_VPS_DIR=/srv/fs-backups
|
||||||
|
BACKUP_VPS_KEY=deploy/backup/id_backup
|
||||||
|
BACKUP_KEEP_LOCAL=14
|
||||||
|
BACKUP_KEEP_REMOTE=30
|
||||||
|
|
||||||
# ─── ПРОЧЕЕ ───────────────────────────────────────────────────────────────────
|
# ─── ПРОЧЕЕ ───────────────────────────────────────────────────────────────────
|
||||||
# Часовой пояс приложения (фикс. смещение в часах; МСК = 3)
|
# Часовой пояс приложения (фикс. смещение в часах; МСК = 3)
|
||||||
APP_TZ_OFFSET_HOURS=3
|
APP_TZ_OFFSET_HOURS=3
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ backend/dev.db*
|
|||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
id_tunnel*
|
id_tunnel*
|
||||||
|
id_backup*
|
||||||
deploy/**/certs/
|
deploy/**/certs/
|
||||||
|
|
||||||
# Node / фронт
|
# Node / фронт
|
||||||
|
|||||||
+54
-11
@@ -15,31 +15,74 @@ ssh-copy-id -i deploy/tunnel/id_tunnel.pub tunnel@186.246.51.17
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 2. .env
|
## 2. .env
|
||||||
|
Все настройки приложения и туннеля — в одном `.env` (контейнер форсит `APP_ENV=production`):
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # заполни секреты; APP_ENV контейнер форсит в production
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
Проверь, что заданы параметры VPS (их читает сервис `tunnel`):
|
Минимально заполнить на Pi:
|
||||||
```
|
```
|
||||||
VPS_TUNNEL_HOST=186.246.51.17
|
SECRET_KEY=... # python -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_USER=tunnel
|
||||||
```
|
```
|
||||||
|
`VPS_TUNNEL_PORT` задавать не нужно — прод-контейнер по умолчанию берёт слот **9000**.
|
||||||
|
Ресурсы (`APP_MEM_LIMIT`/`APP_CPUS`) и оффсайт-бэкап (`BACKUP_*`) — по желанию.
|
||||||
|
|
||||||
## 3. Запуск
|
## 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:
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build # поднимет app + tunnel
|
docker login gitea.arseniev.info # один раз (доступ к пакету)
|
||||||
docker compose ps # оба up; tunnel стартует после healthy app
|
git pull # нужны docker-compose.yml, .env, deploy/tunnel/id_tunnel
|
||||||
docker compose logs -f tunnel # ждём строку: [tunnel] -R 9000:app:8000 -> tunnel@...
|
docker compose pull # тянет свежие образы из реестра
|
||||||
|
docker compose up -d
|
||||||
|
docker compose ps # app healthy → поднимется tunnel
|
||||||
|
docker compose logs -f tunnel # ждём: [tunnel] -R 9000:app:8000 -> tunnel@...
|
||||||
```
|
```
|
||||||
Открой `https://forbiddenstars.ru` — должен отдать прод.
|
Открой `https://forbiddenstars.ru` — должен отдать прод.
|
||||||
|
|
||||||
|
> Альтернатива без реестра (Pi 4/5 ≥4ГБ потянет сборку): `docker compose up -d --build` прямо на Pi.
|
||||||
|
> Реестр Gitea должен быть по HTTPS с валидным сертификатом; иначе на Pi добавь его в
|
||||||
|
> `/etc/docker/daemon.json` → `insecure-registries`.
|
||||||
|
|
||||||
## Обновление
|
## Обновление
|
||||||
```bash
|
```bash
|
||||||
git pull
|
# ПК: scripts/build-push.sh
|
||||||
docker compose up -d --build
|
# Pi: git pull && docker compose pull && docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Бэкап БД
|
## Бэкап (полный + оффсайт на VPS)
|
||||||
`scripts/backup.sh` (консистентная онлайн-копия SQLite; по cron на хосте). Том БД — `db-data`.
|
`scripts/backup.sh` делает консистентный онлайн-снимок и собирает **полный** архив
|
||||||
|
`fs_<TS>.tar.gz` (БД + `uploads/` + `achievements/`) в `backups/` с ротацией. Если в `.env`
|
||||||
|
задан `BACKUP_VPS_HOST` — копия дополнительно уходит на VPS (оффсайт; Pi → VPS push, т.к. CGNAT).
|
||||||
|
|
||||||
|
Настройка оффсайта (один раз):
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Восстановление: `scripts/restore.sh backups/fs_<TS>.tar.gz` (ДЕСТРУКТИВНО — заменяет текущие
|
||||||
|
данные; сначала проверь на тест-контуре). Тома — `db-data` / `uploads-data` / `achievements-data`.
|
||||||
|
|
||||||
Логи: `docker compose logs -f app` / `... tunnel`. Если `https://forbiddenstars.ru` отдаёт
|
Логи: `docker compose logs -f app` / `... tunnel`. Если `https://forbiddenstars.ru` отдаёт
|
||||||
502 — значит туннель не поднят: смотри `docker compose logs tunnel` (чаще всего ключ не в
|
502 — значит туннель не поднят: смотри `docker compose logs tunnel` (чаще всего ключ не в
|
||||||
|
|||||||
@@ -108,3 +108,18 @@ systemctl reload caddy
|
|||||||
(HTTP 503), это ожидаемо.
|
(HTTP 503), это ожидаемо.
|
||||||
|
|
||||||
Логи Caddy: `journalctl -u caddy -f`.
|
Логи Caddy: `journalctl -u caddy -f`.
|
||||||
|
|
||||||
|
## 8. Приём бэкапов с Pi (оффсайт-копии)
|
||||||
|
Pi ежедневно шлёт полный архив (`scripts/backup.sh`) на VPS отдельным пользователем `backup`
|
||||||
|
(не путать с `tunnel`). Каталог — **вне** веб-корней Caddy, права 700.
|
||||||
|
```bash
|
||||||
|
useradd -m -s /bin/bash backup
|
||||||
|
install -d -m 700 -o backup -g backup /home/backup/.ssh
|
||||||
|
install -m 600 -o backup -g backup /dev/null /home/backup/.ssh/authorized_keys
|
||||||
|
install -d -m 700 -o backup -g backup /srv/fs-backups
|
||||||
|
# Публичный ключ бэкапа с Pi (deploy/backup/id_backup.pub) добавь в authorized_keys:
|
||||||
|
# echo '<содержимое id_backup.pub>' >> /home/backup/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
> Ротацию на VPS делает сам скрипт с Pi (`BACKUP_KEEP_REMOTE`, по умолч. 30). Каталог
|
||||||
|
> `/srv/fs-backups` не отдаётся Caddy (нет `root`/`file_server` на него) → бэкапы не публичны.
|
||||||
|
> Проверка приёма: после `scripts/backup.sh` на Pi → `ls -1 /srv/fs-backups/` на VPS.
|
||||||
|
|||||||
+35
-23
@@ -1,58 +1,70 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# ПРОД на Raspberry Pi (forbiddenstars.ru) — единый compose: приложение + туннель.
|
||||||
|
# Все настройки и секреты — в корневом .env (см. .env.example). Этот файл их только
|
||||||
|
# пробрасывает; ничего секретного тут нет.
|
||||||
|
#
|
||||||
|
# Запуск/обновление: docker compose up -d --build
|
||||||
|
# Логи: docker compose logs -f app (или: ... tunnel)
|
||||||
|
# Остановка: docker compose down (данные в томах сохраняются)
|
||||||
|
# Бэкап/restore: scripts/backup.sh / scripts/restore.sh
|
||||||
|
#
|
||||||
|
# Контейнер ВСЕГДА production: APP_ENV форсится здесь и игнорирует значение из .env.
|
||||||
|
# Порты на хост НЕ публикуются — наружу приложение выставляет только сервис tunnel
|
||||||
|
# (SSH reverse-туннель к VPS). Из LAN/localhost оно недоступно.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build: .
|
build: .
|
||||||
image: forbidden-stars:latest
|
# Образ из реестра (Gitea): собирается под arm64 на ПК (scripts/build-push.sh) и тянется
|
||||||
|
# на Pi через `docker compose pull`. build: оставлен как локальный фолбэк (сборка на Pi).
|
||||||
|
image: ${IMAGE_REGISTRY:-gitea.arseniev.info/notbigghost}/forbidden-stars:${IMAGE_TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true # PID 1 пробрасывает сигналы → корректный SIGTERM
|
||||||
|
stop_grace_period: 30s # время на чек-пойнт WAL SQLite перед остановкой
|
||||||
env_file:
|
env_file:
|
||||||
- .env # секреты/настройки (один общий формат для dev и prod)
|
- .env # единый источник настроек/секретов всего приложения
|
||||||
environment:
|
environment:
|
||||||
# Прод обособлен: контейнер ВСЕГДА production и ИГНОРИРУЕТ APP_ENV из .env
|
APP_ENV: production # прод обособлен: игнорируем APP_ENV из .env
|
||||||
# (хоть development, хоть test). БД=/data, STATIC_DIR=/app/static — из Dockerfile.
|
|
||||||
APP_ENV: production
|
|
||||||
# Портов на хост НЕТ: приложение доступно только изнутри сети compose.
|
|
||||||
# Наружу его выставляет сервис tunnel (ниже) через VPS. Из LAN/localhost — недоступно.
|
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/data # БД SQLite + WAL-сайдкары
|
- db-data:/data # БД SQLite + WAL-сайдкары
|
||||||
- uploads-data:/data/uploads # задел под фото/видео
|
- uploads-data:/data/uploads # медиа партий
|
||||||
- achievements-data:/data/achievements # определения ачивок (файлы)
|
- achievements-data:/data/achievements # определения титулов (файлы)
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
|
||||||
- CMD
|
|
||||||
- python
|
|
||||||
- -c
|
|
||||||
- "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"
|
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
mem_limit: 512m
|
mem_limit: ${APP_MEM_LIMIT:-512m} # лимиты под Pi; переопределяются в .env
|
||||||
cpus: 1.5
|
cpus: ${APP_CPUS:-1.5}
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "5"
|
max-file: "5"
|
||||||
|
|
||||||
# SSH reverse-туннель к VPS: forbiddenstars.ru (VPS:9000) -> app:8000 (по сети compose).
|
# SSH reverse-туннель: VPS:${VPS_TUNNEL_PORT:-9000} → app:8000 (домен forbiddenstars.ru).
|
||||||
# Ключ — в ./deploy/tunnel/id_tunnel (gitignore), его pubkey в authorized_keys у tunnel@VPS.
|
# Ключ — ./deploy/tunnel/id_tunnel (в git не идёт); его pubkey в authorized_keys у tunnel@VPS.
|
||||||
# VPS_TUNNEL_HOST/USER берутся из .env (на Pi заполни их там же).
|
|
||||||
tunnel:
|
tunnel:
|
||||||
build: ./deploy/tunnel
|
build: ./deploy/tunnel
|
||||||
image: forbidden-stars-tunnel:latest
|
image: ${IMAGE_REGISTRY:-gitea.arseniev.info/notbigghost}/forbidden-stars-tunnel:${IMAGE_TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
init: true
|
init: true
|
||||||
depends_on:
|
depends_on:
|
||||||
app:
|
app:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
VPS_TUNNEL_HOST: ${VPS_TUNNEL_HOST}
|
VPS_TUNNEL_HOST: ${VPS_TUNNEL_HOST:?VPS_TUNNEL_HOST is required in .env}
|
||||||
VPS_TUNNEL_USER: ${VPS_TUNNEL_USER:-tunnel}
|
VPS_TUNNEL_USER: ${VPS_TUNNEL_USER:-tunnel}
|
||||||
VPS_TUNNEL_PORT: "9000" # forbiddenstars.ru (прод); у test — 9001
|
VPS_TUNNEL_PORT: ${VPS_TUNNEL_PORT:-9000} # прод-слот (forbiddenstars.ru)
|
||||||
UPSTREAM: app:8000
|
UPSTREAM: app:8000
|
||||||
volumes:
|
volumes:
|
||||||
- ./deploy/tunnel/id_tunnel:/key/id_tunnel:ro
|
- ./deploy/tunnel/id_tunnel:/key/id_tunnel:ro
|
||||||
mem_limit: 64m
|
mem_limit: 64m
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-data:
|
db-data:
|
||||||
|
|||||||
Regular → Executable
+61
-15
@@ -1,28 +1,74 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Консистентный онлайн-бэкап SQLite из работающего контейнера.
|
# Полный консистентный онлайн-бэкап прода (БД + uploads + achievements) с работающего
|
||||||
# Запуск по cron на хосте Pi, например ежедневно в 04:00:
|
# контейнера + оффсайт-копия на VPS. Запуск по cron на хосте Pi, например ежедневно в 04:00:
|
||||||
# 0 4 * * * /home/pi/ForbidenStarsApp/scripts/backup.sh >> /var/log/fs-backup.log 2>&1
|
# 0 4 * * * /home/pi/ForbidenStarsApp/scripts/backup.sh >> /var/log/fs-backup.log 2>&1
|
||||||
|
#
|
||||||
|
# Настройки берутся из корневого .env (BACKUP_*), их можно переопределить переменными
|
||||||
|
# окружения. Если BACKUP_VPS_HOST пуст — делается только локальная копия в backups/.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
BACKUP_DIR="${BACKUP_DIR:-$PROJECT_DIR/backups}"
|
COMPOSE="docker compose -f $PROJECT_DIR/docker-compose.yml"
|
||||||
KEEP="${KEEP:-14}"
|
|
||||||
SERVICE="app"
|
SERVICE="app"
|
||||||
TS="$(date +%Y%m%d_%H%M%S)"
|
TS="$(date +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
|
# Безопасно прочитать ОДИН ключ из .env (НЕ source: в .env есть спецсимволы # & и т.п.).
|
||||||
|
read_env() { # $1=ключ $2=дефолт
|
||||||
|
local val
|
||||||
|
val="$(grep -E "^$1=" "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r')"
|
||||||
|
val="${val%\"}"; val="${val#\"}" # снять обрамляющие кавычки, если есть
|
||||||
|
printf '%s' "${val:-$2}"
|
||||||
|
}
|
||||||
|
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-$PROJECT_DIR/backups}"
|
||||||
|
KEEP_LOCAL="${BACKUP_KEEP_LOCAL:-$(read_env BACKUP_KEEP_LOCAL 14)}"
|
||||||
|
KEEP_REMOTE="${BACKUP_KEEP_REMOTE:-$(read_env BACKUP_KEEP_REMOTE 30)}"
|
||||||
|
VPS_HOST="${BACKUP_VPS_HOST:-$(read_env BACKUP_VPS_HOST "")}"
|
||||||
|
VPS_USER="${BACKUP_VPS_USER:-$(read_env BACKUP_VPS_USER backup)}"
|
||||||
|
VPS_DIR="${BACKUP_VPS_DIR:-$(read_env BACKUP_VPS_DIR /srv/fs-backups)}"
|
||||||
|
VPS_KEY="${BACKUP_VPS_KEY:-$(read_env BACKUP_VPS_KEY "$PROJECT_DIR/deploy/backup/id_backup")}"
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
STAGE="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
|
|
||||||
# .backup внутри контейнера (sqlite3 модуль Python) → консистентная копия в /data.
|
CID="$($COMPOSE ps -q "$SERVICE")"
|
||||||
docker compose -f "$PROJECT_DIR/docker-compose.yml" exec -T "$SERVICE" \
|
if [ -z "$CID" ]; then
|
||||||
python -c "import sqlite3; s=sqlite3.connect('/data/forbidden_stars.db'); d=sqlite3.connect('/data/backup_tmp.db'); s.backup(d); d.close(); s.close()"
|
echo "[backup] ОШИБКА: контейнер '$SERVICE' не запущен." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Копируем во вне контейнера и сжимаем.
|
echo "[backup] Консистентный снимок БД…"
|
||||||
CID="$(docker compose -f "$PROJECT_DIR/docker-compose.yml" ps -q "$SERVICE")"
|
$COMPOSE exec -T "$SERVICE" python -c "import sqlite3; s=sqlite3.connect('/data/forbidden_stars.db'); d=sqlite3.connect('/data/backup_tmp.db'); s.backup(d); d.close(); s.close()"
|
||||||
docker cp "$CID:/data/backup_tmp.db" "$BACKUP_DIR/fs_$TS.db"
|
docker cp "$CID:/data/backup_tmp.db" "$STAGE/forbidden_stars.db"
|
||||||
docker compose -f "$PROJECT_DIR/docker-compose.yml" exec -T "$SERVICE" rm -f /data/backup_tmp.db
|
$COMPOSE exec -T "$SERVICE" rm -f /data/backup_tmp.db
|
||||||
gzip -f "$BACKUP_DIR/fs_$TS.db"
|
|
||||||
|
|
||||||
# Ротация: оставить последние $KEEP.
|
echo "[backup] Копирование медиа и титулов…"
|
||||||
ls -1t "$BACKUP_DIR"/fs_*.db.gz 2>/dev/null | tail -n +"$((KEEP + 1))" | xargs -r rm -f
|
docker cp "$CID:/data/uploads" "$STAGE/uploads"
|
||||||
|
docker cp "$CID:/data/achievements" "$STAGE/achievements"
|
||||||
|
|
||||||
echo "[backup] Готово: $BACKUP_DIR/fs_$TS.db.gz"
|
ARCHIVE="$BACKUP_DIR/fs_$TS.tar.gz"
|
||||||
|
tar -czf "$ARCHIVE" -C "$STAGE" forbidden_stars.db uploads achievements
|
||||||
|
echo "[backup] Локальный архив: $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))"
|
||||||
|
|
||||||
|
# Локальная ротация: оставить последние $KEEP_LOCAL (архив только что создан → ≥1 файл).
|
||||||
|
ls -1t "$BACKUP_DIR"/fs_*.tar.gz 2>/dev/null | tail -n +"$((KEEP_LOCAL + 1))" | xargs -r rm -f
|
||||||
|
|
||||||
|
# Оффсайт-копия на VPS (push: Pi за CGNAT, входящих нет).
|
||||||
|
if [ -n "$VPS_HOST" ]; then
|
||||||
|
if [ ! -f "$VPS_KEY" ]; then
|
||||||
|
echo "[backup] ОШИБКА: ключ $VPS_KEY не найден — оффсайт-копия пропущена." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[backup] Отправка на VPS $VPS_USER@$VPS_HOST:$VPS_DIR …"
|
||||||
|
SSH_OPTS=(-i "$VPS_KEY" -o StrictHostKeyChecking=accept-new -o BatchMode=yes)
|
||||||
|
scp "${SSH_OPTS[@]}" "$ARCHIVE" "$VPS_USER@$VPS_HOST:$VPS_DIR/"
|
||||||
|
# Удалённая ротация (sh на VPS без pipefail → пустой ls не уронит команду).
|
||||||
|
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" \
|
||||||
|
"ls -1t '$VPS_DIR'/fs_*.tar.gz 2>/dev/null | tail -n +$((KEEP_REMOTE + 1)) | xargs -r rm -f"
|
||||||
|
echo "[backup] Оффсайт-копия отправлена."
|
||||||
|
else
|
||||||
|
echo "[backup] BACKUP_VPS_HOST не задан — только локальная копия."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[backup] Готово."
|
||||||
|
|||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Build prod images (app + tunnel) for linux/arm64 (Raspberry Pi) and push to the registry.
|
||||||
|
# Run on the DEV machine with Docker Desktop / buildx. Registry and tag come from the root
|
||||||
|
# .env (IMAGE_REGISTRY / IMAGE_TAG) - the same values docker-compose.yml uses.
|
||||||
|
# Log in to the registry once before running: docker login gitea.arseniev.info
|
||||||
|
# Keep this file ASCII-only (Windows PowerShell 5.1 mangles non-ASCII without a BOM).
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$root = Split-Path -Parent $PSScriptRoot
|
||||||
|
Set-Location $root
|
||||||
|
|
||||||
|
# Builder with multi-arch push support (docker-container driver) - create if missing.
|
||||||
|
docker buildx inspect fsbuilder *> $null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
docker buildx create --name fsbuilder --driver docker-container | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# arm64 emulation on an x86 host (usually already present in Docker Desktop; idempotent).
|
||||||
|
docker run --privileged --rm tonistiigi/binfmt --install arm64 | Out-Null
|
||||||
|
|
||||||
|
Write-Host "Building app+tunnel for linux/arm64 and pushing to the registry..." -ForegroundColor Green
|
||||||
|
docker buildx bake -f docker-compose.yml --builder fsbuilder --set "*.platform=linux/arm64" --push
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "Build/push FAILED (exit $LASTEXITCODE)." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Done. On the Pi: docker compose pull && docker compose up -d" -ForegroundColor Green
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Сборка прод-образов (app + tunnel) под linux/arm64 для Raspberry Pi и пуш в реестр.
|
||||||
|
# Запускается на машине РАЗРАБОТКИ с Docker Desktop/buildx (не на Pi).
|
||||||
|
# Реестр и тег берутся из корневого .env (IMAGE_REGISTRY / IMAGE_TAG) — те же, что в
|
||||||
|
# docker-compose.yml. Перед запуском один раз залогиниться в реестр:
|
||||||
|
# docker login gitea.arseniev.info
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# Билдер с поддержкой push разных архитектур (driver docker-container) — создаём при отсутствии.
|
||||||
|
if ! docker buildx inspect fsbuilder >/dev/null 2>&1; then
|
||||||
|
docker buildx create --name fsbuilder --driver docker-container >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Эмуляция arm64 на x86-хосте (в Docker Desktop обычно уже стоит; команда идемпотентна).
|
||||||
|
docker run --privileged --rm tonistiigi/binfmt --install arm64 >/dev/null
|
||||||
|
|
||||||
|
echo "[build-push] Сборка app+tunnel под linux/arm64 и пуш в реестр…"
|
||||||
|
# bake читает docker-compose.yml: контексты build: и теги image: (с подстановкой из .env).
|
||||||
|
docker buildx bake -f docker-compose.yml --builder fsbuilder --set "*.platform=linux/arm64" --push
|
||||||
|
|
||||||
|
echo "[build-push] Готово. На Pi: docker compose pull && docker compose up -d"
|
||||||
Executable
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Восстановление прода из архива бэкапа (fs_<TS>.tar.gz): БД + uploads + achievements.
|
||||||
|
# ДЕСТРУКТИВНО — заменяет текущие данные тома. Сначала проверьте на тест-контуре!
|
||||||
|
# Использование:
|
||||||
|
# scripts/restore.sh backups/fs_YYYYmmdd_HHMMSS.tar.gz [--yes]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
COMPOSE="docker compose -f $PROJECT_DIR/docker-compose.yml"
|
||||||
|
SERVICE="app"
|
||||||
|
|
||||||
|
ARCHIVE="${1:-}"
|
||||||
|
CONFIRM="${2:-}"
|
||||||
|
if [ -z "$ARCHIVE" ] || [ ! -f "$ARCHIVE" ]; then
|
||||||
|
echo "Использование: $0 <путь к fs_*.tar.gz> [--yes]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$CONFIRM" != "--yes" ]; then
|
||||||
|
printf "ВНИМАНИЕ: текущие БД/uploads/achievements будут заменены из %s\nПродолжить? [y/N] " "$ARCHIVE"
|
||||||
|
read -r ans
|
||||||
|
case "$ans" in y|Y|yes|да|Да) ;; *) echo "Отменено."; exit 1 ;; esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
STAGE="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
|
tar -xzf "$ARCHIVE" -C "$STAGE"
|
||||||
|
if [ ! -f "$STAGE/forbidden_stars.db" ]; then
|
||||||
|
echo "ОШИБКА: в архиве нет forbidden_stars.db." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[restore] Остановка приложения…"
|
||||||
|
$COMPOSE stop "$SERVICE"
|
||||||
|
|
||||||
|
echo "[restore] Очистка тома (старая БД, WAL/SHM, медиа, титулы)…"
|
||||||
|
$COMPOSE run --rm --no-deps -T --entrypoint sh "$SERVICE" -c \
|
||||||
|
"rm -f /data/forbidden_stars.db /data/forbidden_stars.db-wal /data/forbidden_stars.db-shm; rm -rf /data/uploads/* /data/achievements/*"
|
||||||
|
|
||||||
|
CID="$($COMPOSE ps -aq "$SERVICE")"
|
||||||
|
echo "[restore] Запись данных в контейнер…"
|
||||||
|
docker cp "$STAGE/forbidden_stars.db" "$CID:/data/forbidden_stars.db"
|
||||||
|
[ -d "$STAGE/uploads" ] && docker cp "$STAGE/uploads/." "$CID:/data/uploads"
|
||||||
|
[ -d "$STAGE/achievements" ] && docker cp "$STAGE/achievements/." "$CID:/data/achievements"
|
||||||
|
|
||||||
|
echo "[restore] Запуск приложения…"
|
||||||
|
$COMPOSE start "$SERVICE"
|
||||||
|
echo "[restore] Готово. Проверьте https://forbiddenstars.ru и логи: $COMPOSE logs -f app"
|
||||||
Reference in New Issue
Block a user