Files
ForbiddenStarsApp/deploy/backup/README.md
T
NotBigGhostandClaude Opus 5 f5f8dc03ce Бэкапы: БД без таблиц не становится снимком, первый бэкап ждёт приложение
guard_empty считал неизвестные счётчики («?» — нет таблиц users/matches)
непустыми, и снимок без данных уходил в оба репозитория и становился latest —
restore latest в сценарии «Pi умер» падал на verify_staging. Теперь это отказ,
как для пустой БД (обход — --allow-empty).

entrypoint перед самым первым бэкапом ждёт /api/health приложения (до
BACKUP_APP_WAIT_SECONDS, 600 с): оно отвечает только после миграций. Не дождались —
пробуем всё равно, guard не пропустит БД без таблиц. README, раздел 9: новые
строки журнала, снимки с «?» больше не создаются. #74

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-19 03:15:49 +03:00

1003 lines
57 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Бэкапы Forbidden Stars
Пошаговая инструкция: как включить бэкапы, проверить, что они работают, скачать их на ПК
и восстановить данные — в том числе на новом Pi, если старый умер.
Команды даны целиком — копируйте их как есть. Где нужно подставить своё значение, это
написано в угловых скобках: `<IP-адрес-Pi>`. Каждый шаг заканчивается блоком
**«Что должно получиться»** — не переходите к следующему шагу, пока не получили то же самое.
## Содержание
0. [Как это устроено](#0-как-это-устроено)
1. [Пароль шифрования](#шаг-1-пароль-шифрования) — ПК
2. [SSH-ключ для VPS](#шаг-2-ssh-ключ-для-vps) — ПК
3. [VPS: пользователь только для SFTP](#шаг-3-vps-пользователь-только-для-sftp) — VPS
4. [Сборка и публикация образов](#шаг-4-сборка-и-публикация-образов) — ПК
5. [Pi: включить бэкапы](#шаг-5-pi-включить-бэкапы) — Pi
6. [ПК: доступ к Pi и выгрузка бэкапов](#шаг-6-пк-доступ-к-pi-и-выгрузка-бэкапов) — ПК
7. [Проверка скачанного архива](#шаг-7-проверка-скачанного-архива) — ПК
8. [Восстановление прода](#8-восстановление-прода) — Pi
9. [Катастрофа: Pi умер](#9-катастрофа-pi-умер) — новый Pi
10. [Повседневные действия](#10-повседневные-действия)
11. [Неполадки](#11-неполадки)
12. [Справочник: команды и переменные](#12-справочник-команды-и-переменные)
13. [Итоговый чек-лист](#13-итоговый-чек-лист)
---
## 0. Как это устроено
```
Raspberry Pi (прод) VPS 186.246.51.17
┌──────────────────────────────────────────┐ ┌─────────────────────────────┐
│ app ──► тома: БД, uploads, achievements │ │ /srv/fs-backups/restic │
│ ▲ читает (консистентно) │ SFTP │ (копия №2, зашифрована) │
│ backup ──────┘ │ ────────────► │ пользователь fsbackup: │
│ │ каждую ночь в 04:00 │ (Pi сам │ только SFTP, без shell │
│ ▼ │ ходит └─────────────────────────────┘
│ том backup-data (копия №1, зашифрована) │ наружу)
└──────────────────────────────────────────┘
▲ ssh + scp по команде «pull»
┌─────────┴────────────────────────────────┐
│ ПК: backups\fs_<дата>_<id>.tar │ копия №3, по запросу, НЕ зашифрована
└──────────────────────────────────────────┘
```
На Pi работает третий контейнер — `backup`. В нём [restic](https://restic.net) — известная
программа для бэкапов. Каждую ночь контейнер:
1. снимает **консистентную** копию БД. Сайт при этом работает, пользователи ничего не замечают;
2. проверяет копию (`PRAGMA integrity_check`). Битая копия не сохраняется, старые снимки
тоже не трогаются;
3. сохраняет **снимок** — БД + загруженные фото (`uploads`) + титулы (`achievements`) — в
два места: на сам Pi и на VPS;
4. удаляет старые снимки по правилам хранения;
5. проверяет, что репозитории целы.
Раз в неделю (воскресенье, 05:30) дополнительно читает часть сохранённых данных (10 %) и
убеждается, что БД из последнего снимка извлекается и проходит проверку целостности.
**Словарь**
| Слово | Что значит |
|---|---|
| **Снимок** (snapshot) | Состояние данных на момент бэкапа. У каждого есть короткий ID, например `3f2a9c1d`. |
| **Репозиторий** | Хранилище снимков. У нас их два: `local` (на Pi) и `vps` (на VPS). |
| **Пароль шифрования** | Им зашифрованы оба репозитория. Без него снимки прочитать нельзя — **никак**. |
| **Хранение** | Сколько снимков остаётся: последний за каждый из 14 дней, за каждую из 8 недель, за каждый из 12 месяцев, плюс 3 самых свежих. Одинаковые данные хранятся один раз (дедупликация), поэтому 30+ снимков занимают немногим больше одного. |
| **Именованный снимок** | Снимок, сделанный вручную с меткой (`--tag before-update`). Автоматически не удаляется. |
| **pre-restore** | Страховочный снимок, который автоматически делается перед каждым восстановлением: «как было до». Автоматически не удаляется. |
**Что попадает в снимок:** БД, `uploads`, `achievements`.
**Что НЕ попадает:** файл `.env` с секретами (пароли, токен бота, ключи). Его копию храните
отдельно — см. [шаг 1](#шаг-1-пароль-шифрования).
**Ограничения, о которых стоит знать**
- Копии на ПК (`backups\*.tar`) **не зашифрованы**: там данные игроков. Не выкладывайте их
никуда и удаляйте ненужные.
- Ключ, которым Pi заходит на VPS, умеет и удалять файлы в `/srv/fs-backups`. Если Pi будет
взломан, злоумышленник сможет удалить копию на VPS. Поэтому раз в месяц полезно скачивать
снимок на ПК ([раздел 10](#10-повседневные-действия)).
---
## Шаг 1. Пароль шифрования
**Где:** ПК. **Сколько времени:** 5 минут.
> ### ⚠️ Самое важное во всей инструкции
> Потеряете пароль — **ни один бэкап не восстановить**. Ни на Pi, ни на VPS. Никакого
> «сброса пароля» у restic нет, и это не баг, а суть шифрования.
1. Откройте PowerShell и сгенерируйте пароль:
```powershell
python -c "import secrets; print(secrets.token_urlsafe(32))"
```
Если `python` не найден, подойдёт такая команда:
```powershell
$b = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); [Convert]::ToBase64String($b).TrimEnd('=').Replace('+','-').Replace('/','_')
```
2. Сохраните результат в менеджер паролей (Bitwarden, KeePass, 1Password…), запись
назовите, например, **«Forbidden Stars — пароль бэкапов (restic)»**.
3. Сделайте вторую копию вне компьютера: распечатайте или запишите на бумагу и уберите
в надёжное место.
4. **Рекомендуется:** в той же записи менеджера паролей храните копию файла `.env` с Pi
(после шага 5). При гибели Pi это сэкономит час восстановления секретов.
**Что должно получиться:** строка из ~43 символов (латиница, цифры, `-`, `_`), сохранённая
в менеджере паролей и на бумаге.
> В пароле не должно быть символа `$` — docker compose воспринимает его как переменную.
> Обе команды выше `$` не генерируют.
---
## Шаг 2. SSH-ключ для VPS
**Где:** ПК, PowerShell, папка репозитория. **Зачем:** этим ключом контейнер `backup`
на Pi будет заходить на VPS. Пароль к ключу не ставим: контейнер работает без человека.
1. Перейдите в папку репозитория (подставьте свой путь):
```powershell
cd C:\Users\<вы>\ForbidenStarsApp
```
2. Создайте ключ:
```powershell
ssh-keygen -t ed25519 -C "fs-backup" -f deploy\backup\id_backup
```
На вопросы `Enter passphrase` и `Enter same passphrase again` просто **дважды нажмите
Enter** (пароль к ключу пустой).
3. Проверьте, что появились два файла:
```powershell
Get-ChildItem deploy\backup\id_backup*
Get-Content deploy\backup\id_backup.pub
```
4. Проверьте, что ключ не попадёт в git (команда должна вывести **пустоту**):
```powershell
git status --short deploy\backup
```
**Что должно получиться:**
- файлы `id_backup` (приватный, секрет) и `id_backup.pub` (публичный);
- содержимое `id_backup.pub` — одна строка вида `ssh-ed25519 AAAAC3Nza…много символов… fs-backup`;
- `git status` по этой папке ничего не показывает.
---
## Шаг 3. VPS: пользователь только для SFTP
**Где:** VPS. **Зачем:** отдельный пользователь `fsbackup` сможет только класть и читать
файлы бэкапов по SFTP: без shell, без туннелей, без входа по паролю.
> Почему `fsbackup`, а не `backup`: в Debian/Ubuntu системный пользователь `backup` уже
> существует (он служебный, домашняя папка `/var/backups`) — его трогать нельзя.
1. Зайдите на VPS (с ПК):
```powershell
ssh root@186.246.51.17
```
Все команды ниже выполняются **на VPS** от root. Заходите не под root — добавляйте
`sudo` перед каждой командой.
2. Убедитесь, что такого пользователя ещё нет:
```bash
id fsbackup
```
Ожидается `id: 'fsbackup': no such user`. Если пользователь уже есть — пропустите пункт 3.
3. Создайте пользователя и папки:
```bash
useradd --create-home --shell /usr/sbin/nologin fsbackup
install -d -m 700 -o fsbackup -g fsbackup /home/fsbackup/.ssh
install -m 600 -o fsbackup -g fsbackup /dev/null /home/fsbackup/.ssh/authorized_keys
install -d -m 700 -o fsbackup -g fsbackup /srv/fs-backups
```
4. Добавьте публичный ключ из шага 2.
- **На ПК**, во втором окне PowerShell в папке репозитория, скопируйте ключ в буфер обмена:
```powershell
Get-Content deploy\backup\id_backup.pub | Set-Clipboard
```
- **На VPS** наберите команду ниже, вставив ключ вместо `ВСТАВЬТЕ_КЛЮЧ`. Вставка в
терминале — правая кнопка мыши или `Ctrl+Shift+V`. Одинарные кавычки оставьте:
```bash
echo 'ВСТАВЬТЕ_КЛЮЧ' >> /home/fsbackup/.ssh/authorized_keys
cat /home/fsbackup/.ssh/authorized_keys
```
`cat` должен показать одну строку, которая начинается с `ssh-ed25519` и заканчивается на `fs-backup`.
5. Запретите этому пользователю всё, кроме SFTP. Скопируйте блок **целиком**, от `cat` до
последнего `EOF` включительно:
```bash
cat > /etc/ssh/sshd_config.d/60-fs-backup.conf <<'EOF'
Match User fsbackup
ForceCommand internal-sftp -d /srv/fs-backups
PasswordAuthentication no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
X11Forwarding no
PermitTTY no
EOF
```
6. Убедитесь, что основной конфиг подключает папку `sshd_config.d`:
```bash
grep -n '^Include' /etc/ssh/sshd_config
```
Ожидается строка `Include /etc/ssh/sshd_config.d/*.conf`. **Если вывода нет** (старая
система), допишите блок в конец основного конфига:
`cat /etc/ssh/sshd_config.d/60-fs-backup.conf >> /etc/ssh/sshd_config`.
7. Проверьте конфиг и примените его. Текущая SSH-сессия при этом не оборвётся:
```bash
sshd -t && echo "конфиг OK"
systemctl reload ssh || systemctl reload sshd
```
Если `sshd -t` вывел ошибку, **не выходите из сессии** и исправьте файл
(`nano /etc/ssh/sshd_config.d/60-fs-backup.conf`). Ошибка в конфиге sshd может закрыть
вход на сервер.
8. Проверьте, что правила действуют только на `fsbackup`:
```bash
sshd -T -C user=fsbackup,host=x,addr=1.2.3.4 | grep -i forcecommand
sshd -T -C user=root,host=x,addr=1.2.3.4 | grep -i forcecommand
```
Первая команда должна показать `forcecommand internal-sftp -d /srv/fs-backups`,
вторая — `forcecommand none`.
9. **С ПК** (новое окно PowerShell в папке репозитория) проверьте вход по SFTP:
```powershell
sftp -i deploy\backup\id_backup fsbackup@186.246.51.17
```
При первом подключении ответьте `yes` на вопрос `Are you sure you want to continue
connecting`. Появится приглашение `sftp>`. Наберите `pwd`, затем `bye`.
10. **С ПК** проверьте, что shell закрыт:
```powershell
ssh -i deploy\backup\id_backup fsbackup@186.246.51.17
```
**Что должно получиться:**
- `sshd -t` → `конфиг OK`;
- в `sftp` команда `pwd` отвечает `Remote working directory: /srv/fs-backups`;
- `ssh` из пункта 10 отвечает `This service allows sftp connections only.` и сразу отключается;
- вход root на VPS работает как раньше (проверьте новым окном: `ssh root@186.246.51.17`).
---
## Шаг 4. Сборка и публикация образов
**Где:** ПК с Docker Desktop, папка репозитория. **Зачем:** на Pi нет сборки — он скачивает
готовые образы из реестра Gitea. Новый образ `forbidden-stars-backup` нужно туда положить.
> `build-push` собирает **все три** образа (app, tunnel, backup) из текущей ветки ПК и
> публикует их с тегом из `IMAGE_TAG`. Убедитесь, что вы на ветке, которая должна быть на
> проде (обычно `main` после релиза): `git branch --show-current`.
1. Один раз войдите в реестр (если уже входили — пропустите):
```powershell
docker login gitea.arseniev.info
```
2. Соберите и опубликуйте образы (первый раз — 5–15 минут):
```powershell
.\scripts\build-push.ps1
```
3. Проверьте, что образ бэкапа есть в реестре и собран под arm64:
```powershell
docker buildx imagetools inspect gitea.arseniev.info/notbigghost/forbidden-stars-backup:latest
```
**Что должно получиться:**
- `build-push.ps1` заканчивается зелёной строкой `Done. On the Pi: ...`;
- `imagetools inspect` показывает `Platform: linux/arm64`.
---
## Шаг 5. Pi: включить бэкапы
**Где:** Pi. **Сколько времени:** 15 минут.
1. Зайдите на Pi и перейдите в папку прода:
```powershell
ssh pi@<IP-адрес-Pi>
```
```bash
cd ~/forbidden-stars
ls
```
Должны быть видны `docker-compose.yml` и `.env`. Если папка называется иначе, дальше
везде используйте своё название.
2. Сохраните копию текущего compose-файла — на случай отката:
```bash
cp docker-compose.yml docker-compose.yml.bak-$(date +%F)
```
3. Скачайте новый `docker-compose.yml`. Замените `main` на `dev`, если функционал ещё не
попал в релиз:
```bash
BRANCH=main
curl -fsSLO https://gitea.arseniev.info/NotBigGhost/ForbiddenStarsApp/raw/branch/$BRANCH/docker-compose.yml
grep -n '^ backup:' docker-compose.yml
```
`grep` должен найти строку ` backup:`. Если не нашёл, в этой ветке функционала ещё нет.
4. Посмотрите, какие `BACKUP_*` уже есть в `.env`:
```bash
grep -n '^BACKUP_' .env
```
- Есть **старые** строки (`BACKUP_VPS_USER=backup`, `BACKUP_VPS_DIR=/srv/fs-backups`,
`BACKUP_VPS_KEY`, `BACKUP_KEEP_LOCAL`, `BACKUP_KEEP_REMOTE`)? **Удалите их все** в
пункте 5: они указывают на неправильного пользователя и папку.
- Уже есть **новые** строки (`BACKUP_PASSWORD`, `BACKUP_SSH_KEY_B64` и т.д. — если `.env`
делался из свежего `.env.example`)? Блок ниже **не дописывайте**, а заполните
существующие строки теми же значениями. Иначе переменные задвоятся.
5. Откройте `.env` и добавьте в конец блок ниже:
```bash
nano .env
```
В nano: стрелками вниз до конца файла, вставка — правая кнопка мыши, сохранить —
`Ctrl+O`, затем `Enter`, выйти — `Ctrl+X`.
```ini
# ─── БЭКАПЫ ───
BACKUP_PASSWORD=<пароль из шага 1>
BACKUP_SCHEDULE="0 4 * * *"
BACKUP_VERIFY_SCHEDULE="30 5 * * 0"
BACKUP_TZ=Europe/Moscow
BACKUP_KEEP_DAILY=14
BACKUP_KEEP_WEEKLY=8
BACKUP_KEEP_MONTHLY=12
BACKUP_COMPRESSION=max
BACKUP_MAX_AGE_HOURS=30
BACKUP_VPS_HOST=186.246.51.17
BACKUP_VPS_USER=fsbackup
BACKUP_VPS_PORT=22
BACKUP_VPS_DIR=/srv/fs-backups/restic
BACKUP_SSH_KEY_B64=<длинная строка base64, см. ниже>
```
**Как получить `BACKUP_SSH_KEY_B64`.** На ПК, в PowerShell в папке репозитория:
```powershell
[Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path "deploy\backup\id_backup"))) | Set-Clipboard
[Text.Encoding]::ASCII.GetString([Convert]::FromBase64String((Get-Clipboard))).Split("`n")[0]
```
Вторая команда проверяет содержимое буфера. Она должна вывести
`-----BEGIN OPENSSH PRIVATE KEY-----`. Теперь вставьте строку из буфера в `.env` после
`BACKUP_SSH_KEY_B64=`: **одной строкой, без пробелов и кавычек**.
**Что означает каждая строка**
| Переменная | Значение | Что делает |
|---|---|---|
| `BACKUP_PASSWORD` | пароль из шага 1 | шифрует оба репозитория; пусто = бэкапы выключены |
| `BACKUP_SCHEDULE` | `"0 4 * * *"` | когда делать бэкап: минута, час, день, месяц, день недели → каждый день в 04:00 |
| `BACKUP_VERIFY_SCHEDULE` | `"30 5 * * 0"` | когда проверять данные: воскресенье 05:30 |
| `BACKUP_TZ` | `Europe/Moscow` | часовой пояс расписания и времени снимков |
| `BACKUP_KEEP_DAILY/WEEKLY/MONTHLY` | `14` / `8` / `12` | сколько дней / недель / месяцев хранить снимки |
| `BACKUP_COMPRESSION` | `max` | сжатие restic: `auto` — быстрее, `max` — плотнее, `off` — без сжатия |
| `BACKUP_MAX_AGE_HOURS` | `30` | если успешного бэкапа не было дольше — контейнер помечается `unhealthy` |
| `BACKUP_VPS_HOST` | `186.246.51.17` | адрес VPS; пусто = копия только на Pi |
| `BACKUP_VPS_USER` | `fsbackup` | пользователь из шага 3 |
| `BACKUP_VPS_PORT` | `22` | SSH-порт VPS |
| `BACKUP_VPS_DIR` | `/srv/fs-backups/restic` | папка репозитория на VPS (создастся сама) |
| `BACKUP_SSH_KEY_B64` | base64 ключа | приватный ключ из шага 2 одной строкой |
6. Проверьте `.env`, **не показывая секреты на экране**:
```bash
grep -c '^BACKUP_' .env
grep '^BACKUP_SSH_KEY_B64=' .env | wc -c
grep '^BACKUP_PASSWORD=' .env | grep -c '\$'
docker compose config --quiet && echo "compose OK"
```
Ожидается:
- `14` (или `16`, если в `.env` есть ещё `BACKUP_PI_SSH` и `BACKUP_PI_DIR` из нового шаблона —
на Pi они не используются и не мешают);
- число больше `400`;
- `0`;
- `compose OK`.
Проверить, что ни одна переменная не задвоилась (вывод должен быть **пустым**):
```bash
grep -o '^BACKUP_[A-Z_0-9]*' .env | sort | uniq -d
```
7. Запустите **только** контейнер бэкапа. Сайт при этом не перезапускается:
```bash
docker compose up -d backup
```
8. Смотрите журнал первого запуска (выход — `Ctrl+C`, контейнер продолжит работать):
```bash
docker compose logs -f backup
```
Первый бэкап начинается сразу. В журнале должны появиться, в таком порядке:
- `Снимков в локальном репозитории ещё нет — делаю первый бэкап сразу.`
- `БД в порядке: игроков N, партий M.`
- `Репозиторий local ещё не создан — создаю …`
- `OK: репозиторий local, снимок xxxxxxxx.`
- `Репозиторий vps ещё не создан — создаю (sftp:fs-vps:/srv/fs-backups/restic …`
- `OK: репозиторий vps, снимок yyyyyyyy.`
- `Бэкап завершён.`
- `Расписание (TZ=Europe/Moscow):` и две строки расписания.
9. Проверьте состояние и хронологию:
```bash
docker compose exec backup fs-backup status
docker compose exec backup fs-backup list
docker compose exec backup fs-backup list vps
docker compose ps backup
```
10. На VPS убедитесь, что копия пришла (там только зашифрованные файлы restic):
```bash
ls -la /srv/fs-backups/restic
```
**Что должно получиться:**
- в `status` у `[local]` и `[vps]` есть строка `Последний бэкап: <сегодня> — снимок …`;
- `list` и `list vps` показывают по одному снимку с верным числом игроков и партий;
- на VPS в `/srv/fs-backups/restic` лежат `config`, `data`, `index`, `keys`, `snapshots`;
- `docker compose ps backup` показывает `Up`. Первые ~10 минут статус `(health: starting)`,
затем `(healthy)`.
---
## Шаг 6. ПК: доступ к Pi и выгрузка бэкапов
**Где:** ПК. **Зачем:** одной командой скачивать снимок с Pi на ПК — третья копия, которая
не зависит ни от Pi, ни от VPS.
1. Проверьте вход на Pi **без пароля**:
```powershell
ssh pi@<IP-адрес-Pi> "echo ok && cd ~/forbidden-stars && docker compose ps backup"
```
Если выводится `ok` и строка контейнера `backup` без запроса пароля — переходите
к пункту 3.
2. Если спрашивает пароль, настройте вход по ключу (один раз).
Создайте ключ ПК, если его ещё нет. На вопросы `passphrase` дважды нажмите Enter:
```powershell
if (-not (Test-Path "$env:USERPROFILE\.ssh\id_ed25519")) { ssh-keygen -t ed25519 -f "$env:USERPROFILE\.ssh\id_ed25519" }
```
Передайте публичный ключ на Pi (пароль Pi спросят в последний раз):
```powershell
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" | ssh pi@<IP-адрес-Pi> "mkdir -p ~/.ssh && chmod 700 ~/.ssh && tr -d '\r' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
```
Повторите пункт 1 — пароль спрашиваться не должен.
3. Добавьте в `.env` **на ПК** (корень репозитория) или исправьте, если строки уже есть:
```ini
BACKUP_PI_SSH=pi@<IP-адрес-Pi>
BACKUP_PI_DIR=~/forbidden-stars
```
4. Проверьте скрипт:
```powershell
.\scripts\fs-backup.ps1 status
.\scripts\fs-backup.ps1 list
```
Если PowerShell пишет `running scripts is disabled on this system`, выполните один раз:
`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`.
5. Скачайте последний снимок:
```powershell
.\scripts\fs-backup.ps1 pull
```
Скрипт делает всё сам:
- выгружает снимок в файл на Pi;
- копирует файл на ПК по `scp`;
- сверяет контрольную сумму sha256;
- проверяет, что внутри есть БД;
- удаляет временный файл на Pi.
6. Посмотрите, что внутри архива:
```powershell
Get-ChildItem backups\fs_*.tar
tar -tf (Get-ChildItem backups\fs_*.tar | Sort-Object LastWriteTime | Select-Object -Last 1).FullName | Select-Object -First 10
```
**Что должно получиться:**
- `pull` заканчивается зелёной строкой `OK: ...\backups\fs_<дата>_<время>_<id>.tar (… MB, N files, sha256 verified)`;
- внутри архива есть `forbidden_stars.db`, `uploads/…`, `achievements/…`.
> Архив — обычный `.tar`, его открывает 7-Zip. БД внутри — файл SQLite, его можно
> посмотреть в [DB Browser for SQLite](https://sqlitebrowser.org). Архив **не зашифрован**.
>
> Другие варианты: снимок с VPS — `.\scripts\fs-backup.ps1 pull -Repo vps`,
> конкретный снимок — `.\scripts\fs-backup.ps1 pull -Snapshot 3f2a9c1d` (ID из `list`).
---
## Шаг 7. Проверка скачанного архива
**Где:** ПК. **Зачем:** убедиться, что БД в скачанном снимке целая и в ней те данные, что
ожидаются, **до** того как это понадобится по-настоящему. Прод не затрагивается.
> Отдельного тестового контейнера для учебного восстановления больше нет. Сам механизм
> `restore`/`import` отрабатывает только на Pi ([раздел 8](#8-восстановление-прода)); здесь
> проверяется содержимое архива.
1. Распакуйте последний скачанный архив во временную папку:
```powershell
$f = (Get-ChildItem backups\fs_*.tar | Sort-Object LastWriteTime | Select-Object -Last 1).FullName
$d = Join-Path $env:TEMP "fs-check"; New-Item -ItemType Directory -Force $d | Out-Null
tar -xf $f -C $d
```
2. Откройте `%TEMP%\fs-check\forbidden_stars.db` в [DB Browser for SQLite](https://sqlitebrowser.org)
(вкладка «Выполнить SQL») и выполните:
```sql
PRAGMA integrity_check;
SELECT (SELECT count(*) FROM users WHERE role = 'player') AS players,
(SELECT count(*) FROM matches) AS matches;
```
3. Закройте DB Browser и удалите временную папку — данные в ней не зашифрованы:
```powershell
Remove-Item -Recurse -Force (Join-Path $env:TEMP "fs-check")
```
**Что должно получиться:**
- `PRAGMA integrity_check` → `ok`;
- `players` и `matches` совпадают со столбцами `Игроков` / `Партий` этого снимка в `list` на Pi;
- в папке рядом с БД есть `uploads\…` (фото партий) и, если заводились, `achievements\…`.
---
## 8. Восстановление прода
**Когда:** данные испорчены или удалены по ошибке, неудачная миграция, «откатить на вчера».
### Как это работает (почему это безопасно)
Восстановление **никогда не пишет поверх текущих данных напрямую**:
1. **Проверка.** Если приложение работает, восстановление отказывается запускаться. Затем
проверяется, хватит ли места на диске.
2. **Разворачивание.** Снимок целиком разворачивается в промежуточную папку
`.restore-new` внутри каждого тома.
3. **Проверка развёрнутого:** целостность БД, наличие таблиц, число файлов совпадает со
снимком. Любая ошибка → промежуточная папка удаляется, **текущие данные не тронуты**.
4. **Страховка.** Текущие данные сохраняются в снимок `pre-restore`.
5. **Замена.** Текущие данные переносятся в `.restore-old`, новые — на их место.
Используется переименование: мгновенно, без копирования. Сбой на этом шаге → всё
возвращается как было.
6. **Уборка.** `.restore-old` удаляется.
### Порядок действий
1. Зайдите на Pi и выберите снимок:
```bash
cd ~/forbidden-stars
docker compose exec backup fs-backup list
```
Пример вывода:
```
ID Время Игроков Партий Размер Прирост Метки
3f2a9c1d 2026-09-13 04:00 12 87 45.1 MB 12.3 KB scheduled
8b1e0f44 2026-09-14 04:00 12 88 45.2 MB 40.1 KB scheduled
```
| Столбец | Что показывает |
|---|---|
| `ID` | ID снимка — его нужно подставить в команду восстановления |
| `Время` | когда сделан снимок |
| `Игроков` / `Партий` | сколько игроков и партий было в БД в этот момент — по ним легко найти «до поломки» |
| `Размер` | полный объём данных снимка |
| `Прирост` | сколько места снимок реально добавил в репозиторий |
| `Метки` | `scheduled` — по расписанию, `manual` — вручную, `pre-restore` — страховочный, `keep` + имя — именованный снимок (`run --tag имя`) |
Если локальный репозиторий повреждён или пуст, смотрите копию на VPS:
`docker compose exec backup fs-backup list vps`.
2. **По желанию, но рекомендуется:** перед восстановлением проверьте выбранный снимок на ПК:
`.\scripts\fs-backup.ps1 pull -Snapshot <ID>`, затем [шаг 7](#шаг-7-проверка-скачанного-архива).
Заодно у вас останется копия этого снимка вне Pi.
3. Остановите приложение. Сайт покажет страницу «Технические шоколадки»:
```bash
docker compose stop app
```
4. Восстановите снимок. С VPS — добавьте в конец `--repo vps`:
```bash
docker compose exec backup fs-backup restore <ID> --yes
```
5. Запустите приложение и проверьте его:
```bash
docker compose start app
docker compose ps
```
Через 30–60 секунд у `app` должно быть `(healthy)`. Откройте `https://forbiddenstars.ru`.
**Что должно получиться:** в выводе `restore` — `Развёрнутые данные в порядке: …`,
`Страховочный снимок: <id>`, `Данные восстановлены.`; сайт работает на данных из снимка.
**Если что-то пошло не так**
- `restore` закончился строкой `ОШИБКА: … текущие данные НЕ тронуты` — данные прежние,
просто запустите приложение (`docker compose start app`) и разберитесь с причиной
([раздел 11](#11-неполадки)).
- Восстановили не тот снимок — верните «как было до»: в `list` найдите самый свежий снимок
с меткой `pre-restore` и восстановите его так же (пункты 3–5).
- После старта сайт показывает заглушку дольше пары минут: `docker compose logs --tail 50 app`,
затем `docker compose restart tunnel`.
### Восстановление из файла-архива
Например, из архива, скачанного на ПК, или из старого `fs_*.tar.gz`.
1. С ПК скопируйте архив на Pi:
```powershell
scp backups\fs_20260914_0400_3f2a9c1d.tar pi@<IP-адрес-Pi>:~/
```
2. На Pi:
```bash
cd ~/forbidden-stars
docker compose cp ~/fs_20260914_0400_3f2a9c1d.tar backup:/import/fs.tar
docker compose stop app
docker compose exec backup fs-backup import /import/fs.tar --yes
docker compose exec backup rm /import/fs.tar
docker compose start app
rm ~/fs_20260914_0400_3f2a9c1d.tar
```
`import` проходит те же проверки и так же делает снимок `pre-restore`.
---
## 9. Катастрофа: Pi умер
**Когда:** Pi сгорел, SD-карта испорчена, Pi украли. Копия на Pi потеряна, остаётся VPS
(и архивы на ПК).
**Что понадобится:**
- пароль бэкапов из менеджера паролей — **без него дальше идти бессмысленно**;
- копия `.env` старого Pi (если сохраняли, [шаг 1](#шаг-1-пароль-шифрования)).
1. Подготовьте новый Pi по [`deploy/pi/README.md`](../pi/README.md), пункты 1–3: Docker,
`docker-compose.yml`, `.env`.
- Есть копия старого `.env` — просто положите её.
- Нет копии — заполните `.env` заново. **`BACKUP_PASSWORD` должен быть прежним.**
Если ключа `deploy/backup/id_backup` на ПК больше нет, сделайте новый (шаг 2), добавьте
его `.pub` на VPS (шаг 3, пункт 4) и укажите новый base64 в `BACKUP_SSH_KEY_B64`.
2. Запустите всё:
```bash
cd ~/forbidden-stars
docker compose up -d
```
3. Дождитесь, пока приложение создаст пустую БД (`docker compose ps` → `app` `(healthy)`).
Контейнер `backup` стартует одновременно с `app`, ждёт, пока приложение ответит (до
10 минут), и пробует сделать первый бэкап. В его журнале (`docker compose logs backup`)
нормально увидеть одно из трёх:
- если приложение так и не поднялось и БД нет:
```
ОШИБКА: БД /fs-db/forbidden_stars.db не найдена — приложение ещё ни разу не запускалось?
Первый бэкап не удался — следующая попытка по расписанию.
```
- если БД есть, но без таблиц (миграции ещё не прошли):
```
Не удалось прочитать число игроков и партий: в БД нет таблиц users/matches.
```
- если БД уже была:
```
В БД нет ни игроков, ни партий, а последний снимок в репозитории vps — с данными.
Похоже на новый или очищенный сервер: бэкап НЕ сделан, чтобы пустые данные не вытеснили историю.
```
Это защита: пустой новый Pi не перезапишет историю на VPS.
4. Посмотрите снимки на VPS и выберите нужный (обычно самый свежий **с данными**):
```bash
docker compose exec backup fs-backup list vps
```
Проверьте, что у выбранного снимка в столбцах `Игроков`/`Партий` числа, а не `0` и не `?`.
Снимки без данных защита больше не создаёт, но снимок с `?` мог остаться от старой версии
бэкапов — такой не выбирайте.
5. Восстановите, подставив ID из `list vps`:
```bash
docker compose stop app
docker compose exec backup fs-backup restore <ID> --repo vps --yes
docker compose start app
```
Если самый свежий снимок в `list vps` — с данными, вместо ID можно написать `latest`.
6. Проверьте сайт. Затем сделайте снимок вручную, чтобы локальная копия появилась сразу:
```bash
docker compose exec backup fs-backup run
docker compose exec backup fs-backup status
```
**Что должно получиться:** на сайте прежние данные; `run` проходит (`OK: репозиторий local…`,
`OK: репозиторий vps…`); в `status` оба репозитория с сегодняшним бэкапом.
> **VPS тоже недоступен?** Восстанавливайте из последнего архива на ПК: скопируйте его на
> новый Pi и выполните [восстановление из файла-архива](#восстановление-из-файла-архива).
---
## 10. Повседневные действия
| Задача | На Pi (`cd ~/forbidden-stars`) | С ПК (папка репозитория) |
|---|---|---|
| Состояние бэкапов | `docker compose exec backup fs-backup status` | `.\scripts\fs-backup.ps1 status` |
| Хронология снимков | `docker compose exec backup fs-backup list` (`list vps`) | `.\scripts\fs-backup.ps1 list` (`-Repo vps`) |
| Снимок перед рискованным обновлением | `docker compose exec backup fs-backup run --tag before-update` | `.\scripts\fs-backup.ps1 now -Tag before-update` |
| Проверить целостность данных | `docker compose exec backup fs-backup verify` | `.\scripts\fs-backup.ps1 verify` |
| Скачать снимок на ПК | — | `.\scripts\fs-backup.ps1 pull` |
| Журнал контейнера | `docker compose logs --tail 100 backup` | — |
**Рекомендуемый ритм**
- **Перед каждым обновлением прода** — `now -Tag before-update` (метка — латиница, цифры, `.`, `_`, `-`).
- **Раз в месяц:**
- скачать снимок на ПК (`pull`);
- проверить скачанный архив ([шаг 7](#шаг-7-проверка-скачанного-архива));
- удалить с ПК старые архивы — они не зашифрованы.
- **Иногда:** посмотреть `docker compose ps`. Статус `unhealthy` у `backup` означает, что
бэкапы перестали проходить (причину покажет `fs-backup status`).
**Изменить расписание или сроки хранения.** Отредактируйте `BACKUP_*` в `.env` на Pi, затем
пересоздайте контейнер:
```bash
docker compose up -d backup
```
**Удалить именованный или pre-restore снимок.** Автоматически они не удаляются, удалять
нужно в обоих репозиториях:
```bash
docker compose exec backup fs-backup restic local forget <ID> --prune
docker compose exec backup fs-backup restic vps forget <ID-на-vps> --prune
```
ID одного и того же снимка в `local` и `vps` разные — смотрите `list` и `list vps`.
**Обновить образ бэкапа** (после изменений в `deploy/backup/`). На ПК —
`.\scripts\build-push.ps1`, на Pi — `docker compose up -d backup`.
> **Никогда не выполняйте на проде `docker compose down -v`.** Флаг `-v` удаляет тома —
> данные приложения **и** локальную копию бэкапов. Обычный `docker compose down` данные
> не трогает.
---
## 11. Неполадки
Первое, что стоит сделать при любой проблеме:
```bash
cd ~/forbidden-stars
docker compose exec backup fs-backup status
docker compose logs --tail 100 backup
```
### На Pi (журнал и команды `fs-backup`)
| Симптом | Причина | Что сделать |
|---|---|---|
| `BACKUP_PASSWORD не задан в .env — бэкапы ОТКЛЮЧЕНЫ` | нет пароля в `.env` | добавить `BACKUP_PASSWORD` (шаг 5), затем `docker compose up -d backup` |
| `неверный BACKUP_PASSWORD для репозитория …` | пароль в `.env` не тот, с которым создан репозиторий | вернуть правильный пароль из менеджера паролей; `docker compose up -d backup` |
| `BACKUP_SSH_KEY_B64 не декодируется из base64` / `— не приватный SSH-ключ` | строка ключа обрезана, с пробелами или от `.pub` | заново скопировать base64 **приватного** ключа (шаг 5, пункт 5), одной строкой |
| `Репозиторий vps недоступен` и выше `Permission denied (publickey)` | на VPS нет публичного ключа или ключ другой | шаг 3, пункты 4 и 9: проверить `authorized_keys` и вход `sftp` с ПК этим ключом |
| `Host key verification failed` | VPS переустановлен, у него новый ключ хоста | `docker compose exec backup rm /backup/state/known_hosts`, затем `docker compose exec backup fs-backup run` |
| `Репозиторий vps недоступен` и `Connection timed out` | VPS недоступен или неверный `BACKUP_VPS_HOST`/`PORT` | проверить VPS; локальная копия при этом продолжает делаться |
| `В БД нет ни игроков, ни партий, а последний снимок … — с данными` | новый или очищенный сервер — защита от затирания истории | новый Pi: [раздел 9](#9-катастрофа-pi-умер). Данные удалены намеренно: `fs-backup run --allow-empty` |
| `копия БД не прошла PRAGMA integrity_check` | живая БД повреждена | снимок не создаётся, старые целы. Восстановить последний хороший снимок ([раздел 8](#8-восстановление-прода)) |
| `приложение работает — восстанавливать поверх него нельзя` | не остановлен `app` | `docker compose stop app`, повторить команду |
| `найдены следы прерванного восстановления` | восстановление оборвалось (выключили питание и т.п.) | `docker compose stop app`, `docker compose exec backup fs-backup recover`, затем при необходимости повторить `restore` |
| `уже выполняется другая операция бэкапа` | идёт бэкап по расписанию или проверка | подождать: `docker compose logs -f backup` |
| `мало места в …` | диск Pi заполнен | `df -h`; удалить неиспользуемые образы (`docker image prune`); уменьшить `BACKUP_KEEP_*` |
| `снимок '…' не найден в репозитории` | опечатка в ID или снимок в другом репозитории | `fs-backup list` и `fs-backup list vps` |
| `service "backup" is not running` | контейнер не запущен | `docker compose up -d backup` |
| `permission denied while trying to connect to the Docker daemon socket` | пользователь Pi не в группе `docker` | `sudo usermod -aG docker $USER`, перезайти по SSH |
| `docker compose ps` → `backup` `unhealthy` | последний успешный бэкап старше `BACKUP_MAX_AGE_HOURS`, успешных бэкапов ещё не было или не задан `BACKUP_PASSWORD` | `docker compose exec backup fs-backup health` покажет причину, дальше по таблице |
**Про `recover`.** Команда сама определяет, на каком этапе оборвалось восстановление:
- если замена успела пройти во всех томах — дочищает промежуточные папки, восстановленные
данные остаются;
- иначе возвращает каждому тому прежние данные.
После неё можно спокойно запускать приложение или повторить восстановление.
**Пароль бэкапов потерян навсегда.** Прочитать существующие снимки невозможно. Начать заново
(**это удалит все старые бэкапы**):
```bash
docker compose stop backup
docker compose rm -f backup
docker volume ls | grep backup-data # имя тома, обычно forbidden-stars_backup-data
docker volume rm <имя тома>
```
На VPS: `rm -rf /srv/fs-backups/restic`. Затем новый пароль в `.env`,
`docker compose up -d backup`.
### На ПК (скрипт `fs-backup.ps1`)
| Симптом | Причина | Что сделать |
|---|---|---|
| `running scripts is disabled on this system` | политика запуска PowerShell | `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` |
| `Set BACKUP_PI_SSH in .env` | не заполнен адрес Pi | шаг 6, пункт 3 |
| `ssh: connect to host … Connection timed out` | неверный IP Pi или ПК не в той сети | проверить `ssh pi@<IP-адрес-Pi>` вручную |
| Пароль Pi спрашивается несколько раз за команду | не настроен вход по ключу | шаг 6, пункт 2 |
| `WARNING: UNPROTECTED PRIVATE KEY FILE!` | у файла ключа слишком открытые права (ключ создан в Git Bash/WSL или скопирован) | `icacls <путь к ключу> /inheritance:r /grant:r "$($env:USERNAME):(R)"` |
| `Checksum mismatch … run pull again` | файл повредился при передаче | повторить `pull` (битый файл уже удалён) |
| `Already downloaded: …` | этот снимок уже скачан | ничего не делать; нужен новый — сначала `now`, потом `pull` |
| шаг 7: `integrity_check` не `ok` или счётчики не совпадают с `list` | архив повреждён или скачан не тот снимок | удалить файл из `backups\` и скачать заново (`pull -Snapshot <ID>`); если повторяется — `fs-backup verify` на Pi |
---
## 12. Справочник: команды и переменные
### `fs-backup` — внутри контейнера
Запуск на Pi из папки прода: `docker compose exec backup fs-backup <команда>`.
| Команда | Что делает |
|---|---|
| `status` | расписание, хранение, последний бэкап и проверка по каждому репозиторию, размеры |
| `list [local\|vps]` | хронология снимков |
| `run [--tag имя] [--allow-empty]` | снимок сейчас; `--tag` — именованный (не удаляется) |
| `verify` | проверка данных репозиториев и открываемости БД последнего снимка |
| `restore <ID\|latest> [--repo local\|vps] --yes` | восстановление снимка (приложение должно быть остановлено) |
| `import /import/<файл> --yes` | восстановление из `.tar` или старого `.tar.gz` |
| `recover` | разбор прерванного восстановления |
| `export <ID\|latest> [--repo …]` | снимок в tar в stdout: `docker compose exec -T backup fs-backup export latest > fs.tar` |
| `restic <local\|vps> <аргументы>` | любая команда restic с настройками контейнера, например `restic vps stats` |
| `help` | краткая справка |
| `init`, `info`, `health`, `has-snapshots` | служебные: создать репозитории, данные снимка для скриптов ПК, healthcheck, проверка «есть ли снимки» при старте |
Без `--yes` команды `restore` и `import` только показывают, что собираются сделать.
Флаг `--no-pre-restore` у `restore`/`import` пропускает страховочный снимок — используйте,
только если текущие данные точно не нужны.
### Скрипт ПК
`.\scripts\fs-backup.ps1` (Windows) или `scripts/fs-backup.sh` (Linux/macOS/Git Bash):
| Команда | Что делает |
|---|---|
| `status`, `list [-Repo vps]`, `now [-Tag имя]`, `verify` | то же, что на Pi, но с ПК |
| `pull [-Snapshot ID] [-Repo vps]` | скачать снимок в `backups\` со сверкой sha256 |
В bash-версии те же команды пишутся так: `list vps`, `now --tag имя`,
`pull <ID> --repo vps`.
### Переменные `.env`
| Переменная | Где | По умолчанию | Назначение |
|---|---|---|---|
| `BACKUP_PASSWORD` | Pi | — | пароль шифрования; пусто = бэкапы выключены |
| `BACKUP_SCHEDULE` | Pi | `0 4 * * *` | расписание бэкапа (cron) |
| `BACKUP_VERIFY_SCHEDULE` | Pi | `30 5 * * 0` | расписание проверки данных |
| `BACKUP_TZ` | Pi | `Europe/Moscow` | часовой пояс |
| `BACKUP_KEEP_DAILY` / `WEEKLY` / `MONTHLY` | Pi | `14` / `8` / `12` | глубина хранения |
| `BACKUP_COMPRESSION` | Pi | `max` | сжатие restic: `auto` / `max` / `off` |
| `BACKUP_MAX_AGE_HOURS` | Pi | `30` | порог `unhealthy` |
| `BACKUP_VPS_HOST` | Pi | — | адрес VPS; пусто = без оффсайт-копии |
| `BACKUP_VPS_USER` | Pi | `fsbackup` | SFTP-пользователь |
| `BACKUP_VPS_PORT` | Pi | `22` | SSH-порт VPS |
| `BACKUP_VPS_DIR` | Pi | `/srv/fs-backups/restic` | папка репозитория на VPS |
| `BACKUP_SSH_KEY_B64` | Pi | — | приватный ключ для VPS, base64 |
| `BACKUP_MEM_LIMIT` | Pi | `384m` | лимит памяти контейнера |
| `BACKUP_PI_SSH` | ПК | — | как зайти на Pi: `pi@<IP>` |
| `BACKUP_PI_DIR` | ПК | `~/forbidden-stars` | папка прода на Pi |
У скрипта ПК переменная окружения с тем же именем важнее значения из `.env`.
### Где что лежит
| Что | Где |
|---|---|
| Локальный репозиторий | том `backup-data` на Pi → `/backup/repo` в контейнере |
| Состояние (последние запуски, ключ хоста VPS) | том `backup-data` → `/backup/state` |
| Репозиторий на VPS | `/srv/fs-backups/restic` (пользователь `fsbackup`) |
| Архивы на ПК | `backups\` в папке репозитория (в git не попадают) |
| Ключ для VPS на ПК | `deploy\backup\id_backup` (в git не попадает) |
| Код | `deploy/backup/` (образ, `fs-backup.sh`), `scripts/fs-backup.ps1` / `.sh` |
---
## 13. Итоговый чек-лист
**Настройка**
- [ ] Пароль бэкапов сохранён в менеджере паролей **и** на бумаге (шаг 1)
- [ ] Копия `.env` с Pi сохранена в менеджере паролей (шаг 1, пункт 4)
- [ ] Ключ `deploy\backup\id_backup` создан, в git не попадает (шаг 2)
- [ ] На VPS пользователь `fsbackup`, `sftp` работает, shell закрыт (шаг 3)
- [ ] Образы опубликованы, `forbidden-stars-backup` есть под arm64 (шаг 4)
- [ ] На Pi первый бэкап прошёл в `local` и `vps`, `status` без ошибок (шаг 5)
- [ ] `docker compose ps` показывает `backup` `(healthy)` (шаг 5)
- [ ] С ПК `status`, `list`, `pull` работают без пароля (шаг 6)
- [ ] Скачанный архив проверен: БД целая, числа совпадают с `list` (шаг 7)
**Через сутки**
- [ ] В `list` появился снимок с меткой `scheduled` в 04:00
- [ ] В `list vps` — такой же
**Через неделю**
- [ ] В `status` есть строка `Последняя проверка данных: … данные целы`