Настройка бэкапа (#64) #66

Merged
NotBigGhost merged 5 commits from issue-64-backup into dev 2026-09-14 17:54:01 +03:00
Collaborator

Что сделано

Старые scripts/backup.sh и restore.sh на хосте Pi заменены третьим контейнером backup на restic. Он каждую ночь делает снимок БД, uploads и achievements в два репозитория: на Pi (том backup-data) и на VPS (SFTP).

  • Снимки зашифрованы и дедуплицированы. Сжатие делает только restic (репозиторий v2, BACKUP_COMPRESSION).
  • Хронология с числом игроков и партий в каждом снимке.
  • Хранение: 14 дней, 8 недель, 12 месяцев и 3 последних снимка. Именованные и pre-restore снимки не удаляются.
  • Раз в неделю — проверка данных.
  • Healthcheck: контейнер помечается unhealthy, если бэкап давно не проходил.

На Pi по-прежнему нужны только docker-compose.yml и .env: ключ для VPS лежит в .env в base64, секреты приложения в контейнер бэкапа не передаются.

Восстановление идёт через промежуточную директорию:

  1. Если app работает — отказ.
  2. Проверка свободного места.
  3. Снимок разворачивается в .restore-new внутри каждого тома.
  4. Проверка развёрнутого: integrity_check, ключевые таблицы, число файлов, для архивов — целостность tar.
  5. Страховочный снимок pre-restore.
  6. Двухфазная замена через rename с файлом фазы: при сбое данные откатываются, а после обрыва питания их возвращает fs-backup recover.

Заодно исправлен баг старого restore.sh: docker cp делал файлы root-овыми, и после восстановления БД становилась доступна только для чтения.

Скрипты ПК scripts/fs-backup.ps1 и scripts/fs-backup.sh:

  • status, list, now, verify;
  • pull — снимок с Pi копируется в backups\ через scp со сверкой sha256;
  • restore-test — учебное восстановление в тест-клон.

Подробная пошаговая инструкция — deploy/backup/README.md: пароль, ключ, VPS, образы, Pi, ПК, учебное восстановление, восстановление прода, гибель Pi, неполадки, справочник, чек-лист.

Коммиты

  • f73316f Бэкап: образ restic-сайдкара и скрипт fs-backup
  • e2d729d Бэкап: сервис backup в compose прода и тест-клона, .env.example
  • d764ff8 Бэкап: скрипты управления с ПК
  • 19f6068 Бэкап: защита истории от пустых данных
  • 8b400b7 Бэкап: убрать старые backup.sh/restore.sh, документация

Проверки

Всё прогонялось на локальном стенде: отдельный compose-проект, контейнер-«VPS» с sshd, настроенный командами из README, и контейнер-«Pi» с sshd и docker CLI. Тома пользователя не затрагивались, стенд удалён.

Статические проверки

  • shellcheck (fs-backup.sh, entrypoint.sh, scripts/fs-backup.sh) — чисто.
  • .ps1: 0 не-ASCII байт, 0 ошибок разбора.
  • docker compose config для трёх compose-файлов — OK.
  • Сборка образа под amd64 и arm64; под эмуляцией arm64 запускаются restic, sqlite, supercronic, tini.
  • pytest — 147 passed (бэкенд не менялся).

Бэкап

  • Первый снимок в local и vps, репозитории создаются сами.
  • Повторный снимок без изменений добавил 794 байта.
  • Сжатие: экономия 30–38%.
  • Хранение: 104 снимка задним числом → остались ожидаемые дневные, недельные и месячные, а также keep и pre-restore.
  • verify проходит.

Восстановление

  • Восстановление при работающем app — отказ.
  • restore после порчи данных:
    • данные вернулись, лишние файлы удалены;
    • владелец файлов — 10001;
    • app healthy, запись в БД проходит;
    • создан pre-restore.
  • restore --repo vps работает.
  • Сбои не меняют данные (хэш до и после одинаковый): обрезанный архив (посередине и ровно по границе файла), архив с битой БД, сбой замены в фазе A, сбой замены в фазе B.
  • recover после имитации обрыва питания: откат незавершённой замены и дочистка завершённой.
  • import: .tar из export, .tar.gz старого формата — в том числе настоящий архив прежнего backup.sh из backups/; app поднялся с миграциями.

Сценарий «Pi умер»

  • Пустой новый Pi: бэкап отказан, история на VPS цела.
  • restore latest --repo vps → 5 игроков → следующий бэкап и healthcheck OK.

Точка входа

  • Без пароля: простой без рестарт-петли, unhealthy.
  • При пустом репозитории первый снимок делается сразу.
  • Расписание supercronic срабатывает.

Скрипты ПК через ssh к «Pi»

  • status, list, now -Tag.
  • pull: sha256 сходится, повторный pull пропускает скачанное, временный файл на Pi удаляется, ошибка на несуществующем снимке понятная.
  • restore-test, -Target test.
  • Устойчивость к 2>&1 в PowerShell 5.1.
  • bash-версия — те же сценарии.

Команды README

  • Раздел VPS извлечён из README и выполнен буквально в чистом Debian: sshd -t OK, Match действует только на fsbackup, shell закрыт, SFTP открывается в /srv/fs-backups.
  • Проверки шага 5 на копии «папки Pi» дают указанные в README значения.

Отклонения от плана

  • Пользователь VPS — fsbackup, а не backup. В Debian и Ubuntu системный backup (uid 34, /var/backups) уже существует — старая инструкция на реальном VPS не сработала бы.
  • Добавлена защита от пустых данных (run отказывается при пустой БД, если в истории есть данные; обход — --allow-empty) и --keep-last 3. Иначе новый Pi до восстановления отправил бы пустой снимок на VPS: тот стал бы latest, а keep-daily мог вытеснить настоящий снимок того же дня.
  • Добавлены именованные снимки (run --tag): политика хранения их не удаляет.
  • Добавлена команда recover, у замены появились фазы. Первая версия отката при сбое в первой фазе могла удалить ещё не перенесённые старые файлы — найдено при тестах и исправлено до коммита.
  • Не проверено (на стенде недоступно): настоящий ssh до Pi и VPS, запуск на реальном железе Pi. Эмуляция покрывает те же команды.

Что сделать вручную после мёржа

Всё по шагам в deploy/backup/README.md:

  1. Пароль.
  2. Ключ.
  3. Пользователь fsbackup на VPS.
  4. build-push.ps1.
  5. На Pi: новый compose и блок BACKUP_* в .env (старые строки BACKUP_* удалить), docker compose up -d backup.
  6. BACKUP_PI_SSH в .env ПК.
  7. Учебный restore-test.

Closes #64

🤖 Generated with Claude Code

https://claude.ai/code/session_013jBxs9nBCk5nBdTLzcGz91

## Что сделано Старые `scripts/backup.sh` и `restore.sh` на хосте Pi заменены третьим контейнером `backup` на **restic**. Он каждую ночь делает снимок БД, `uploads` и `achievements` в два репозитория: на Pi (том `backup-data`) и на VPS (SFTP). - Снимки зашифрованы и дедуплицированы. Сжатие делает только restic (репозиторий v2, `BACKUP_COMPRESSION`). - Хронология с числом игроков и партий в каждом снимке. - Хранение: 14 дней, 8 недель, 12 месяцев и 3 последних снимка. Именованные и `pre-restore` снимки не удаляются. - Раз в неделю — проверка данных. - Healthcheck: контейнер помечается unhealthy, если бэкап давно не проходил. На Pi по-прежнему нужны только `docker-compose.yml` и `.env`: ключ для VPS лежит в `.env` в base64, секреты приложения в контейнер бэкапа не передаются. **Восстановление идёт через промежуточную директорию:** 1. Если `app` работает — отказ. 2. Проверка свободного места. 3. Снимок разворачивается в `.restore-new` внутри каждого тома. 4. Проверка развёрнутого: `integrity_check`, ключевые таблицы, число файлов, для архивов — целостность tar. 5. Страховочный снимок `pre-restore`. 6. Двухфазная замена через `rename` с файлом фазы: при сбое данные откатываются, а после обрыва питания их возвращает `fs-backup recover`. Заодно исправлен баг старого `restore.sh`: `docker cp` делал файлы root-овыми, и после восстановления БД становилась доступна только для чтения. **Скрипты ПК** `scripts/fs-backup.ps1` и `scripts/fs-backup.sh`: - `status`, `list`, `now`, `verify`; - `pull` — снимок с Pi копируется в `backups\` через scp со сверкой sha256; - `restore-test` — учебное восстановление в тест-клон. **Подробная пошаговая инструкция** — `deploy/backup/README.md`: пароль, ключ, VPS, образы, Pi, ПК, учебное восстановление, восстановление прода, гибель Pi, неполадки, справочник, чек-лист. ## Коммиты - `f73316f` Бэкап: образ restic-сайдкара и скрипт fs-backup - `e2d729d` Бэкап: сервис backup в compose прода и тест-клона, .env.example - `d764ff8` Бэкап: скрипты управления с ПК - `19f6068` Бэкап: защита истории от пустых данных - `8b400b7` Бэкап: убрать старые backup.sh/restore.sh, документация ## Проверки Всё прогонялось на локальном стенде: отдельный compose-проект, контейнер-«VPS» с sshd, настроенный командами из README, и контейнер-«Pi» с sshd и docker CLI. Тома пользователя не затрагивались, стенд удалён. **Статические проверки** - `shellcheck` (`fs-backup.sh`, `entrypoint.sh`, `scripts/fs-backup.sh`) — чисто. - `.ps1`: 0 не-ASCII байт, 0 ошибок разбора. - `docker compose config` для трёх compose-файлов — OK. - Сборка образа под amd64 и arm64; под эмуляцией arm64 запускаются restic, sqlite, supercronic, tini. - `pytest` — 147 passed (бэкенд не менялся). **Бэкап** - Первый снимок в `local` и `vps`, репозитории создаются сами. - Повторный снимок без изменений добавил 794 байта. - Сжатие: экономия 30–38%. - Хранение: 104 снимка задним числом → остались ожидаемые дневные, недельные и месячные, а также `keep` и `pre-restore`. - `verify` проходит. **Восстановление** - Восстановление при работающем `app` — отказ. - `restore` после порчи данных: - данные вернулись, лишние файлы удалены; - владелец файлов — 10001; - `app` healthy, запись в БД проходит; - создан `pre-restore`. - `restore --repo vps` работает. - Сбои не меняют данные (хэш до и после одинаковый): обрезанный архив (посередине и ровно по границе файла), архив с битой БД, сбой замены в фазе A, сбой замены в фазе B. - `recover` после имитации обрыва питания: откат незавершённой замены и дочистка завершённой. - `import`: `.tar` из `export`, `.tar.gz` старого формата — в том числе **настоящий** архив прежнего `backup.sh` из `backups/`; `app` поднялся с миграциями. **Сценарий «Pi умер»** - Пустой новый Pi: бэкап отказан, история на VPS цела. - `restore latest --repo vps` → 5 игроков → следующий бэкап и healthcheck OK. **Точка входа** - Без пароля: простой без рестарт-петли, unhealthy. - При пустом репозитории первый снимок делается сразу. - Расписание supercronic срабатывает. **Скрипты ПК через ssh к «Pi»** - `status`, `list`, `now -Tag`. - `pull`: sha256 сходится, повторный `pull` пропускает скачанное, временный файл на Pi удаляется, ошибка на несуществующем снимке понятная. - `restore-test`, `-Target test`. - Устойчивость к `2>&1` в PowerShell 5.1. - bash-версия — те же сценарии. **Команды README** - Раздел VPS извлечён из README и выполнен буквально в чистом Debian: `sshd -t` OK, `Match` действует только на `fsbackup`, shell закрыт, SFTP открывается в `/srv/fs-backups`. - Проверки шага 5 на копии «папки Pi» дают указанные в README значения. ## Отклонения от плана - **Пользователь VPS — `fsbackup`, а не `backup`.** В Debian и Ubuntu системный `backup` (uid 34, `/var/backups`) уже существует — старая инструкция на реальном VPS не сработала бы. - **Добавлена защита от пустых данных** (`run` отказывается при пустой БД, если в истории есть данные; обход — `--allow-empty`) и `--keep-last 3`. Иначе новый Pi до восстановления отправил бы пустой снимок на VPS: тот стал бы `latest`, а `keep-daily` мог вытеснить настоящий снимок того же дня. - **Добавлены именованные снимки** (`run --tag`): политика хранения их не удаляет. - **Добавлена команда `recover`, у замены появились фазы.** Первая версия отката при сбое в первой фазе могла удалить ещё не перенесённые старые файлы — найдено при тестах и исправлено до коммита. - **Не проверено** (на стенде недоступно): настоящий ssh до Pi и VPS, запуск на реальном железе Pi. Эмуляция покрывает те же команды. ## Что сделать вручную после мёржа Всё по шагам в `deploy/backup/README.md`: 1. Пароль. 2. Ключ. 3. Пользователь `fsbackup` на VPS. 4. `build-push.ps1`. 5. На Pi: новый compose и блок `BACKUP_*` в `.env` (**старые строки `BACKUP_*` удалить**), `docker compose up -d backup`. 6. `BACKUP_PI_SSH` в `.env` ПК. 7. Учебный `restore-test`. Closes #64 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013jBxs9nBCk5nBdTLzcGz91
Agent added 5 commits 2026-09-14 01:30:15 +03:00
deploy/backup: образ на restic/restic:0.19.1 (+ sqlite, supercronic, tini), работает от
uid 10001, как appuser. Скрипт fs-backup:
- run: консистентная копия БД (VACUUM INTO + integrity_check), снимок в локальный
  репозиторий и на VPS (SFTP), теги players/matches, GFS-очистка (дни/недели/месяцы;
  именованные keep и pre-restore не удаляются), restic check;
- list / status / verify / export (tar без сжатия; сжатие — только restic, репозиторий v2);
- restore / import через промежуточную директорию: разворачивание в .restore-new внутри
  каждого тома, проверка (integrity_check, таблицы, число файлов, целостность tar),
  страховочный снимок pre-restore, двухфазная замена rename с файлом фазы и откатом;
  recover разбирает прерванное восстановление. Отказ при работающем app.
Точка входа: без BACKUP_PASSWORD простой без рестарт-петли, первый снимок при пустом
репозитории, расписание supercronic, healthcheck по возрасту последнего успеха. #64

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jBxs9nBCk5nBdTLzcGz91
docker-compose.yml: сервис backup (образ из реестра, явный environment только с BACKUP_*,
тома данных + backup-data), предупреждение про down -v. docker-compose.test.yml: тот же
образ локальной сборкой, только локальный репозиторий, без расписания, BACKUP_VPS_HOST
принудительно пуст. .env.example: новый блок BACKUP_* (пароль, расписание, хранение,
сжатие, SFTP-пользователь fsbackup — системный backup в Debian/Ubuntu уже занят, ключ
base64, адрес Pi для скриптов ПК); удалены BACKUP_VPS_KEY и BACKUP_KEEP_LOCAL/REMOTE.
build-push: сообщения про третий образ (bake подхватывает сервис сам). #64

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jBxs9nBCk5nBdTLzcGz91
scripts/fs-backup.ps1 (ASCII, Windows PowerShell 5.1) и scripts/fs-backup.sh (Linux/macOS/
Git Bash) — одинаковые команды к контейнеру backup прода на Pi по SSH или к локальному
тест-клону (-Target test / --test): status, list, now [--tag], verify;
pull — export в файл на Pi, scp в backups/, сверка sha256 и проверка содержимого tar
(бинарные данные не идут через пайпы PowerShell — они их портят);
restore-test — учебное восстановление архива (в т.ч. старого fs_*.tar.gz) в тест-клон.
Настройки BACKUP_PI_SSH / BACKUP_PI_DIR из .env, переменная окружения важнее.
Вызовы нативных команд устойчивы к перенаправлению stderr в PS 5.1. #64

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jBxs9nBCk5nBdTLzcGz91
run отказывается бэкапить БД без игроков и партий, если последний снимок в репозитории был
с данными: новый Pi до восстановления иначе отправил бы пустой снимок на VPS, он стал бы
latest, а keep-daily мог вытеснить настоящий снимок того же дня. Обход — run --allow-empty.
Очистка дополнительно всегда оставляет 3 последних снимка. Проверено сценарием «Pi умер»:
отказ бэкапа → restore latest --repo vps → следующий бэкап проходит. #64

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jBxs9nBCk5nBdTLzcGz91
deploy/backup/README.md — подробная пошаговая инструкция по всему, что делается вручную:
пароль шифрования, SSH-ключ, VPS (пользователь fsbackup только для SFTP, проверки sshd),
сборка образов, включение на Pi (с разбором каждой переменной и контрольными проверками),
доступ с ПК и выгрузка, учебное восстановление на тест-клоне, восстановление прода и из
архива, катастрофа «Pi умер», повседневные действия, таблица неполадок, справочник, чек-лист.
Команды разделов VPS и Pi прогнаны на локальном стенде.
deploy/pi, deploy/vps §8, deploy/README — ссылки на новую схему. Удалены scripts/backup.sh и
scripts/restore.sh (restore.sh ещё и оставлял БД root-овой: docker cp пишет файлы с uid 0);
их архивы fs_*.tar.gz восстанавливаются через fs-backup import. #64

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jBxs9nBCk5nBdTLzcGz91
NotBigGhost merged commit 7579ca2d35 into dev 2026-09-14 17:54:01 +03:00
NotBigGhost deleted branch issue-64-backup 2026-09-14 17:54:01 +03:00
Sign in to join this conversation.