# Бэкапы Forbidden Stars Пошаговая инструкция: как включить бэкапы, проверить, что они работают, скачать их на ПК и восстановить данные — в том числе на новом 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_<дата>_.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@ ``` ```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@ "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@ "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@ 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_<дата>_<время>_.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 `, затем [шаг 7](#шаг-7-проверка-скачанного-архива). Заодно у вас останется копия этого снимка вне Pi. 3. Остановите приложение. Сайт покажет страницу «Технические шоколадки»: ```bash docker compose stop app ``` 4. Восстановите снимок. С VPS — добавьте в конец `--repo vps`: ```bash docker compose exec backup fs-backup restore --yes ``` 5. Запустите приложение и проверьте его: ```bash docker compose start app docker compose ps ``` Через 30–60 секунд у `app` должно быть `(healthy)`. Откройте `https://forbiddenstars.ru`. **Что должно получиться:** в выводе `restore` — `Развёрнутые данные в порядке: …`, `Страховочный снимок: `, `Данные восстановлены.`; сайт работает на данных из снимка. **Если что-то пошло не так** - `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@:~/ ``` 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 --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 --prune docker compose exec backup fs-backup restic vps forget --prune ``` ID одного и того же снимка в `local` и `vps` разные — смотрите `list` и `list vps`. **Обновить образ бэкапа** (после изменений в `deploy/backup/`). На ПК — `.\scripts\build-push.ps1`, на Pi — `docker compose up -d backup`. ### Отчёты в Telegram Бот приложения (тот же, что для входа через Telegram) может присылать вам отчёты о бэкапах и отвечать на команды. Пока `BACKUP_TELEGRAM_CHAT_ID` пуст, отчёты выключены. **Что приходит само:** - после каждого бэкапа — «✅ Бэкап»: сколько игроков и партий в БД, по каждому репозиторию снимок, число снимков и размер; - если бэкап не удался, в том числе не начавшись (нет пароля, занята другая операция, БД не прошла проверку) — «❌ Бэкап не удался» с причиной и упавшими репозиториями; - после еженедельной проверки — «🔍 Проверка данных: OK» или «❌». **Команды боту** (отвечает только чатам из `BACKUP_TELEGRAM_CHAT_ID`): - `/backups` — хранящиеся снимки: по репозиторию число и размер, 10 последних с временем, игроками и партиями, 📌 у именованных; - `/status` — то же, что `fs-backup status`; - `/help` — список команд. **Настройка:** 1. В Telegram найдите бота приложения и напишите ему `/start`. 2. Узнайте id своего чата: ```bash docker compose exec backup fs-backup telegram chats # 123456789 @you /start ``` Когда бот уже работает (id вписан), он сам забирает сообщения. Тогда id нового чата ищите в журнале: `docker compose logs backup | grep "чужого чата"`. 3. Впишите id в `.env` — `BACKUP_TELEGRAM_CHAT_ID=123456789`, несколько через запятую — и пересоздайте контейнер: `docker compose up -d backup`. 4. Проверьте связь: `docker compose exec backup fs-backup telegram test` — в чат придёт пробное сообщение. Токен бота по умолчанию берётся из `TELEGRAM_BOT_TOKEN` приложения. Чтобы слать отчёты от другого бота, задайте `BACKUP_TELEGRAM_BOT_TOKEN`. Если Telegram недоступен, бэкапы работают как обычно — в журнале будет только строка «Telegram: … не отправлено». > **Никогда не выполняйте на проде `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` | | отчёты в Telegram не приходят, `telegram test` пишет «отправить не удалось» | неверный chat id или токен, боту не писали `/start` | раздел 10, «Отчёты в Telegram»: написать боту, взять id из журнала, `docker compose up -d backup` | | бот не отвечает на `/backups`, в журнале «сообщение из чужого чата» | ваш id не в `BACKUP_TELEGRAM_CHAT_ID` | вписать id из этой строки журнала, `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@` вручную | | Пароль 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 `); если повторяется — `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 [--repo local\|vps] --yes` | восстановление снимка (приложение должно быть остановлено) | | `import /import/<файл> --yes` | восстановление из `.tar` или старого `.tar.gz` | | `recover` | разбор прерванного восстановления | | `export [--repo …]` | снимок в tar в stdout: `docker compose exec -T backup fs-backup export latest > fs.tar` | | `restic <аргументы>` | любая команда restic с настройками контейнера, например `restic vps stats` | | `telegram chats` | id чатов, писавших боту, — для `BACKUP_TELEGRAM_CHAT_ID` | | `telegram test` | пробное сообщение в чаты `BACKUP_TELEGRAM_CHAT_ID` | | `telegram bot` | бот-слушатель команд `/backups`, `/status` (контейнер запускает его сам) | | `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 --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_TELEGRAM_CHAT_ID` | Pi | — | id чатов для отчётов и команд, через запятую; пусто = без Telegram | | `BACKUP_TELEGRAM_BOT_TOKEN` | Pi | `TELEGRAM_BOT_TOKEN` | токен бота, если отчёты должен слать другой бот | | `BACKUP_MEM_LIMIT` | Pi | `384m` | лимит памяти контейнера | | `BACKUP_PI_SSH` | ПК | — | как зайти на Pi: `pi@` | | `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` есть строка `Последняя проверка данных: … данные целы`