Старые 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, секреты приложения в контейнер бэкапа не передаются.
Восстановление идёт через промежуточную директорию:
Если app работает — отказ.
Проверка свободного места.
Снимок разворачивается в .restore-new внутри каждого тома.
Проверка развёрнутого: integrity_check, ключевые таблицы, число файлов, для архивов — целостность tar.
Страховочный снимок pre-restore.
Двухфазная замена через 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 — учебное восстановление в тест-клон.
f73316f Бэкап: образ restic-сайдкара и скрипт fs-backup
e2d729d Бэкап: сервис backup в compose прода и тест-клона, .env.example
d764ff8 Бэкап: скрипты управления с ПК
19f6068 Бэкап: защита истории от пустых данных
8b400b7 Бэкап: убрать старые backup.sh/restore.sh, документация
Проверки
Всё прогонялось на локальном стенде: отдельный compose-проект, контейнер-«VPS» с sshd, настроенный командами из README, и контейнер-«Pi» с sshd и docker CLI. Тома пользователя не затрагивались, стенд удалён.
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:
Пароль.
Ключ.
Пользователь fsbackup на VPS.
build-push.ps1.
На Pi: новый compose и блок BACKUP_* в .env (старые строки BACKUP_* удалить), docker compose up -d backup.
## Что сделано
Старые `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
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Что сделано
Старые
scripts/backup.shиrestore.shна хосте Pi заменены третьим контейнеромbackupна restic. Он каждую ночь делает снимок БД,uploadsиachievementsв два репозитория: на Pi (томbackup-data) и на VPS (SFTP).BACKUP_COMPRESSION).pre-restoreснимки не удаляются.На Pi по-прежнему нужны только
docker-compose.ymlи.env: ключ для VPS лежит в.envв base64, секреты приложения в контейнер бэкапа не передаются.Восстановление идёт через промежуточную директорию:
appработает — отказ..restore-newвнутри каждого тома.integrity_check, ключевые таблицы, число файлов, для архивов — целостность tar.pre-restore.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-backupe2d729dБэкап: сервис backup в compose прода и тест-клона, .env.exampled764ff8Бэкап: скрипты управления с ПК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.pytest— 147 passed (бэкенд не менялся).Бэкап
localиvps, репозитории создаются сами.keepиpre-restore.verifyпроходит.Восстановление
app— отказ.restoreпосле порчи данных:apphealthy, запись в БД проходит;pre-restore.restore --repo vpsработает.recoverпосле имитации обрыва питания: откат незавершённой замены и дочистка завершённой.import:.tarизexport,.tar.gzстарого формата — в том числе настоящий архив прежнегоbackup.shизbackups/;appподнялся с миграциями.Сценарий «Pi умер»
restore latest --repo vps→ 5 игроков → следующий бэкап и healthcheck OK.Точка входа
Скрипты ПК через ssh к «Pi»
status,list,now -Tag.pull: sha256 сходится, повторныйpullпропускает скачанное, временный файл на Pi удаляется, ошибка на несуществующем снимке понятная.restore-test,-Target test.2>&1в PowerShell 5.1.Команды README
sshd -tOK,Matchдействует только наfsbackup, shell закрыт, SFTP открывается в/srv/fs-backups.Отклонения от плана
fsbackup, а неbackup. В Debian и Ubuntu системныйbackup(uid 34,/var/backups) уже существует — старая инструкция на реальном VPS не сработала бы.runотказывается при пустой БД, если в истории есть данные; обход —--allow-empty) и--keep-last 3. Иначе новый Pi до восстановления отправил бы пустой снимок на VPS: тот стал быlatest, аkeep-dailyмог вытеснить настоящий снимок того же дня.run --tag): политика хранения их не удаляет.recover, у замены появились фазы. Первая версия отката при сбое в первой фазе могла удалить ещё не перенесённые старые файлы — найдено при тестах и исправлено до коммита.Что сделать вручную после мёржа
Всё по шагам в
deploy/backup/README.md:fsbackupна VPS.build-push.ps1.BACKUP_*в.env(старые строкиBACKUP_*удалить),docker compose up -d backup.BACKUP_PI_SSHв.envПК.restore-test.Closes #64
🤖 Generated with Claude Code
https://claude.ai/code/session_013jBxs9nBCk5nBdTLzcGz91