Каждый run и verify шлёт в чаты BACKUP_TELEGRAM_CHAT_ID одно сообщение: успех со сводкой (игроки/партии, по репозиторию снимок, число снимков, размер) или сбой с причиной — в том числе ранний отказ (нет пароля, занят lock, битая БД): обработчик EXIT берёт текст последнего die. Недоступный Telegram бэкап не ломает. Бот-слушатель (fs-backup telegram bot, long polling, стартует из entrypoint в фоне) отвечает на /backups (хранящиеся снимки), /status и /help только чатам из белого списка; чужие chat id пишет в журнал. telegram chats — узнать свой id, telegram test — проверить связь. Токен по умолчанию — TELEGRAM_BOT_TOKEN приложения (тот же бот), BACKUP_TELEGRAM_BOT_TOKEN переопределяет. В образ добавлен curl. Настройка — deploy/backup/README.md, раздел 10. #83 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
61 KiB
Бэкапы Forbidden Stars
Пошаговая инструкция: как включить бэкапы, проверить, что они работают, скачать их на ПК и восстановить данные — в том числе на новом Pi, если старый умер.
Команды даны целиком — копируйте их как есть. Где нужно подставить своё значение, это
написано в угловых скобках: <IP-адрес-Pi>. Каждый шаг заканчивается блоком
«Что должно получиться» — не переходите к следующему шагу, пока не получили то же самое.
Содержание
- Как это устроено
- Пароль шифрования — ПК
- SSH-ключ для VPS — ПК
- VPS: пользователь только для SFTP — VPS
- Сборка и публикация образов — ПК
- Pi: включить бэкапы — Pi
- ПК: доступ к Pi и выгрузка бэкапов — ПК
- Проверка скачанного архива — ПК
- Восстановление прода — Pi
- Катастрофа: Pi умер — новый Pi
- Повседневные действия
- Неполадки
- Справочник: команды и переменные
- Итоговый чек-лист
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 — известная
программа для бэкапов. Каждую ночь контейнер:
- снимает консистентную копию БД. Сайт при этом работает, пользователи ничего не замечают;
- проверяет копию (
PRAGMA integrity_check). Битая копия не сохраняется, старые снимки тоже не трогаются; - сохраняет снимок — БД + загруженные фото (
uploads) + титулы (achievements) — в два места: на сам Pi и на VPS; - удаляет старые снимки по правилам хранения;
- проверяет, что репозитории целы.
Раз в неделю (воскресенье, 05:30) дополнительно читает часть сохранённых данных (10 %) и убеждается, что БД из последнего снимка извлекается и проходит проверку целостности.
Словарь
| Слово | Что значит |
|---|---|
| Снимок (snapshot) | Состояние данных на момент бэкапа. У каждого есть короткий ID, например 3f2a9c1d. |
| Репозиторий | Хранилище снимков. У нас их два: local (на Pi) и vps (на VPS). |
| Пароль шифрования | Им зашифрованы оба репозитория. Без него снимки прочитать нельзя — никак. |
| Хранение | Сколько снимков остаётся: последний за каждый из 14 дней, за каждую из 8 недель, за каждый из 12 месяцев, плюс 3 самых свежих. Одинаковые данные хранятся один раз (дедупликация), поэтому 30+ снимков занимают немногим больше одного. |
| Именованный снимок | Снимок, сделанный вручную с меткой (--tag before-update). Автоматически не удаляется. |
| pre-restore | Страховочный снимок, который автоматически делается перед каждым восстановлением: «как было до». Автоматически не удаляется. |
Что попадает в снимок: БД, uploads, achievements.
Что НЕ попадает: файл .env с секретами (пароли, токен бота, ключи). Его копию храните
отдельно — см. шаг 1.
Ограничения, о которых стоит знать
- Копии на ПК (
backups\*.tar) не зашифрованы: там данные игроков. Не выкладывайте их никуда и удаляйте ненужные. - Ключ, которым Pi заходит на VPS, умеет и удалять файлы в
/srv/fs-backups. Если Pi будет взломан, злоумышленник сможет удалить копию на VPS. Поэтому раз в месяц полезно скачивать снимок на ПК (раздел 10).
Шаг 1. Пароль шифрования
Где: ПК. Сколько времени: 5 минут.
⚠️ Самое важное во всей инструкции
Потеряете пароль — ни один бэкап не восстановить. Ни на Pi, ни на VPS. Никакого «сброса пароля» у restic нет, и это не баг, а суть шифрования.
-
Откройте PowerShell и сгенерируйте пароль:
python -c "import secrets; print(secrets.token_urlsafe(32))"Если
pythonне найден, подойдёт такая команда:$b = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); [Convert]::ToBase64String($b).TrimEnd('=').Replace('+','-').Replace('/','_') -
Сохраните результат в менеджер паролей (Bitwarden, KeePass, 1Password…), запись назовите, например, «Forbidden Stars — пароль бэкапов (restic)».
-
Сделайте вторую копию вне компьютера: распечатайте или запишите на бумагу и уберите в надёжное место.
-
Рекомендуется: в той же записи менеджера паролей храните копию файла
.envс Pi (после шага 5). При гибели Pi это сэкономит час восстановления секретов.
Что должно получиться: строка из ~43 символов (латиница, цифры, -, _), сохранённая
в менеджере паролей и на бумаге.
В пароле не должно быть символа
$— docker compose воспринимает его как переменную. Обе команды выше$не генерируют.
Шаг 2. SSH-ключ для VPS
Где: ПК, PowerShell, папка репозитория. Зачем: этим ключом контейнер backup
на Pi будет заходить на VPS. Пароль к ключу не ставим: контейнер работает без человека.
-
Перейдите в папку репозитория (подставьте свой путь):
cd C:\Users\<вы>\ForbidenStarsApp -
Создайте ключ:
ssh-keygen -t ed25519 -C "fs-backup" -f deploy\backup\id_backupНа вопросы
Enter passphraseиEnter same passphrase againпросто дважды нажмите Enter (пароль к ключу пустой). -
Проверьте, что появились два файла:
Get-ChildItem deploy\backup\id_backup* Get-Content deploy\backup\id_backup.pub -
Проверьте, что ключ не попадёт в git (команда должна вывести пустоту):
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) — его трогать нельзя.
-
Зайдите на VPS (с ПК):
ssh root@186.246.51.17Все команды ниже выполняются на VPS от root. Заходите не под root — добавляйте
sudoперед каждой командой. -
Убедитесь, что такого пользователя ещё нет:
id fsbackupОжидается
id: 'fsbackup': no such user. Если пользователь уже есть — пропустите пункт 3. -
Создайте пользователя и папки:
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 -
Добавьте публичный ключ из шага 2.
-
На ПК, во втором окне PowerShell в папке репозитория, скопируйте ключ в буфер обмена:
Get-Content deploy\backup\id_backup.pub | Set-Clipboard -
На VPS наберите команду ниже, вставив ключ вместо
ВСТАВЬТЕ_КЛЮЧ. Вставка в терминале — правая кнопка мыши илиCtrl+Shift+V. Одинарные кавычки оставьте:echo 'ВСТАВЬТЕ_КЛЮЧ' >> /home/fsbackup/.ssh/authorized_keys cat /home/fsbackup/.ssh/authorized_keys
catдолжен показать одну строку, которая начинается сssh-ed25519и заканчивается наfs-backup. -
-
Запретите этому пользователю всё, кроме SFTP. Скопируйте блок целиком, от
catдо последнегоEOFвключительно: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 -
Убедитесь, что основной конфиг подключает папку
sshd_config.d: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. -
Проверьте конфиг и примените его. Текущая SSH-сессия при этом не оборвётся:
sshd -t && echo "конфиг OK" systemctl reload ssh || systemctl reload sshdЕсли
sshd -tвывел ошибку, не выходите из сессии и исправьте файл (nano /etc/ssh/sshd_config.d/60-fs-backup.conf). Ошибка в конфиге sshd может закрыть вход на сервер. -
Проверьте, что правила действуют только на
fsbackup: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. -
С ПК (новое окно PowerShell в папке репозитория) проверьте вход по SFTP:
sftp -i deploy\backup\id_backup fsbackup@186.246.51.17При первом подключении ответьте
yesна вопросAre you sure you want to continue connecting. Появится приглашениеsftp>. Наберитеpwd, затемbye. -
С ПК проверьте, что shell закрыт:
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.
-
Один раз войдите в реестр (если уже входили — пропустите):
docker login gitea.arseniev.info -
Соберите и опубликуйте образы (первый раз — 5–15 минут):
.\scripts\build-push.ps1 -
Проверьте, что образ бэкапа есть в реестре и собран под arm64:
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 минут.
-
Зайдите на Pi и перейдите в папку прода:
ssh pi@<IP-адрес-Pi>cd ~/forbidden-stars lsДолжны быть видны
docker-compose.ymlи.env. Если папка называется иначе, дальше везде используйте своё название. -
Сохраните копию текущего compose-файла — на случай отката:
cp docker-compose.yml docker-compose.yml.bak-$(date +%F) -
Скачайте новый
docker-compose.yml. Заменитеmainнаdev, если функционал ещё не попал в релиз:BRANCH=main curl -fsSLO https://gitea.arseniev.info/NotBigGhost/ForbiddenStarsApp/raw/branch/$BRANCH/docker-compose.yml grep -n '^ backup:' docker-compose.ymlgrepдолжен найти строкуbackup:. Если не нашёл, в этой ветке функционала ещё нет. -
Посмотрите, какие
BACKUP_*уже есть в.env: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)? Блок ниже не дописывайте, а заполните существующие строки теми же значениями. Иначе переменные задвоятся.
- Есть старые строки (
-
Откройте
.envи добавьте в конец блок ниже:nano .envВ nano: стрелками вниз до конца файла, вставка — правая кнопка мыши, сохранить —
Ctrl+O, затемEnter, выйти —Ctrl+X.# ─── БЭКАПЫ ─── 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 в папке репозитория:[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_TZEurope/Moscowчасовой пояс расписания и времени снимков BACKUP_KEEP_DAILY/WEEKLY/MONTHLY14/8/12сколько дней / недель / месяцев хранить снимки BACKUP_COMPRESSIONmaxсжатие restic: auto— быстрее,max— плотнее,off— без сжатияBACKUP_MAX_AGE_HOURS30если успешного бэкапа не было дольше — контейнер помечается unhealthyBACKUP_VPS_HOST186.246.51.17адрес VPS; пусто = копия только на Pi BACKUP_VPS_USERfsbackupпользователь из шага 3 BACKUP_VPS_PORT22SSH-порт VPS BACKUP_VPS_DIR/srv/fs-backups/resticпапка репозитория на VPS (создастся сама) BACKUP_SSH_KEY_B64base64 ключа приватный ключ из шага 2 одной строкой -
Проверьте
.env, не показывая секреты на экране: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.
Проверить, что ни одна переменная не задвоилась (вывод должен быть пустым):
grep -o '^BACKUP_[A-Z_0-9]*' .env | sort | uniq -d -
Запустите только контейнер бэкапа. Сайт при этом не перезапускается:
docker compose up -d backup -
Смотрите журнал первого запуска (выход —
Ctrl+C, контейнер продолжит работать):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):и две строки расписания.
-
Проверьте состояние и хронологию:
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 -
На VPS убедитесь, что копия пришла (там только зашифрованные файлы restic):
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.
-
Проверьте вход на Pi без пароля:
ssh pi@<IP-адрес-Pi> "echo ok && cd ~/forbidden-stars && docker compose ps backup"Если выводится
okи строка контейнераbackupбез запроса пароля — переходите к пункту 3. -
Если спрашивает пароль, настройте вход по ключу (один раз).
Создайте ключ ПК, если его ещё нет. На вопросы
passphraseдважды нажмите Enter:if (-not (Test-Path "$env:USERPROFILE\.ssh\id_ed25519")) { ssh-keygen -t ed25519 -f "$env:USERPROFILE\.ssh\id_ed25519" }Передайте публичный ключ на Pi (пароль Pi спросят в последний раз):
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 — пароль спрашиваться не должен.
-
Добавьте в
.envна ПК (корень репозитория) или исправьте, если строки уже есть:BACKUP_PI_SSH=pi@<IP-адрес-Pi> BACKUP_PI_DIR=~/forbidden-stars -
Проверьте скрипт:
.\scripts\fs-backup.ps1 status .\scripts\fs-backup.ps1 listЕсли PowerShell пишет
running scripts is disabled on this system, выполните один раз:Set-ExecutionPolicy -Scope CurrentUser RemoteSigned. -
Скачайте последний снимок:
.\scripts\fs-backup.ps1 pullСкрипт делает всё сам:
- выгружает снимок в файл на Pi;
- копирует файл на ПК по
scp; - сверяет контрольную сумму sha256;
- проверяет, что внутри есть БД;
- удаляет временный файл на Pi.
-
Посмотрите, что внутри архива:
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. Архив не зашифрован.Другие варианты: снимок с VPS —
.\scripts\fs-backup.ps1 pull -Repo vps, конкретный снимок —.\scripts\fs-backup.ps1 pull -Snapshot 3f2a9c1d(ID изlist).
Шаг 7. Проверка скачанного архива
Где: ПК. Зачем: убедиться, что БД в скачанном снимке целая и в ней те данные, что ожидаются, до того как это понадобится по-настоящему. Прод не затрагивается.
Отдельного тестового контейнера для учебного восстановления больше нет. Сам механизм
restore/importотрабатывает только на Pi (раздел 8); здесь проверяется содержимое архива.
-
Распакуйте последний скачанный архив во временную папку:
$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 -
Откройте
%TEMP%\fs-check\forbidden_stars.dbв DB Browser for SQLite (вкладка «Выполнить SQL») и выполните:PRAGMA integrity_check; SELECT (SELECT count(*) FROM users WHERE role = 'player') AS players, (SELECT count(*) FROM matches) AS matches; -
Закройте DB Browser и удалите временную папку — данные в ней не зашифрованы:
Remove-Item -Recurse -Force (Join-Path $env:TEMP "fs-check")
Что должно получиться:
PRAGMA integrity_check→ok;playersиmatchesсовпадают со столбцамиИгроков/Партийэтого снимка вlistна Pi;- в папке рядом с БД есть
uploads\…(фото партий) и, если заводились,achievements\….
8. Восстановление прода
Когда: данные испорчены или удалены по ошибке, неудачная миграция, «откатить на вчера».
Как это работает (почему это безопасно)
Восстановление никогда не пишет поверх текущих данных напрямую:
- Проверка. Если приложение работает, восстановление отказывается запускаться. Затем проверяется, хватит ли места на диске.
- Разворачивание. Снимок целиком разворачивается в промежуточную папку
.restore-newвнутри каждого тома. - Проверка развёрнутого: целостность БД, наличие таблиц, число файлов совпадает со снимком. Любая ошибка → промежуточная папка удаляется, текущие данные не тронуты.
- Страховка. Текущие данные сохраняются в снимок
pre-restore. - Замена. Текущие данные переносятся в
.restore-old, новые — на их место. Используется переименование: мгновенно, без копирования. Сбой на этом шаге → всё возвращается как было. - Уборка.
.restore-oldудаляется.
Порядок действий
-
Зайдите на Pi и выберите снимок:
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Столбец Что показывает IDID снимка — его нужно подставить в команду восстановления Времякогда сделан снимок Игроков/Партийсколько игроков и партий было в БД в этот момент — по ним легко найти «до поломки» Размерполный объём данных снимка Приростсколько места снимок реально добавил в репозиторий Меткиscheduled— по расписанию,manual— вручную,pre-restore— страховочный,keep+ имя — именованный снимок (run --tag имя)Если локальный репозиторий повреждён или пуст, смотрите копию на VPS:
docker compose exec backup fs-backup list vps. -
По желанию, но рекомендуется: перед восстановлением проверьте выбранный снимок на ПК:
.\scripts\fs-backup.ps1 pull -Snapshot <ID>, затем шаг 7. Заодно у вас останется копия этого снимка вне Pi. -
Остановите приложение. Сайт покажет страницу «Технические шоколадки»:
docker compose stop app -
Восстановите снимок. С VPS — добавьте в конец
--repo vps:docker compose exec backup fs-backup restore <ID> --yes -
Запустите приложение и проверьте его:
docker compose start app docker compose psЧерез 30–60 секунд у
appдолжно быть(healthy). Откройтеhttps://forbiddenstars.ru.
Что должно получиться: в выводе restore — Развёрнутые данные в порядке: …,
Страховочный снимок: <id>, Данные восстановлены.; сайт работает на данных из снимка.
Если что-то пошло не так
restoreзакончился строкойОШИБКА: … текущие данные НЕ тронуты— данные прежние, просто запустите приложение (docker compose start app) и разберитесь с причиной (раздел 11).- Восстановили не тот снимок — верните «как было до»: в
listнайдите самый свежий снимок с меткойpre-restoreи восстановите его так же (пункты 3–5). - После старта сайт показывает заглушку дольше пары минут:
docker compose logs --tail 50 app, затемdocker compose restart tunnel.
Восстановление из файла-архива
Например, из архива, скачанного на ПК, или из старого fs_*.tar.gz.
-
С ПК скопируйте архив на Pi:
scp backups\fs_20260914_0400_3f2a9c1d.tar pi@<IP-адрес-Pi>:~/ -
На Pi:
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).
-
Подготовьте новый Pi по
deploy/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.
- Есть копия старого
-
Запустите всё:
cd ~/forbidden-stars docker compose up -d -
Дождитесь, пока приложение создаст пустую БД (
docker compose ps→app(healthy)). Контейнерbackupстартует одновременно сappи сразу пробует сделать первый бэкап. В его журнале (docker compose logs backup) нормально увидеть одно из двух:-
если
backupуспел раньше, чемappсоздал БД:ОШИБКА: БД /fs-db/forbidden_stars.db не найдена — приложение ещё ни разу не запускалось? Первый бэкап не удался — следующая попытка по расписанию. -
если БД уже была:
В БД нет ни игроков, ни партий, а последний снимок в репозитории vps — с данными. Похоже на новый или очищенный сервер: бэкап НЕ сделан, чтобы пустые данные не вытеснили историю.Это защита: пустой новый Pi не перезапишет историю на VPS.
-
-
Посмотрите снимки на VPS и выберите нужный (обычно самый свежий с данными):
docker compose exec backup fs-backup list vpsПроверьте, что у выбранного снимка в столбцах
Игроков/Партийчисла, а не0и не?. Защита выше не распознаёт БД без таблиц: если первый бэкап попал ровно в момент создания БД, в списке может появиться свежий снимок с?(задача #74). Такой снимок не выбирайте. -
Восстановите, подставив ID из
list vps:docker compose stop app docker compose exec backup fs-backup restore <ID> --repo vps --yes docker compose start appЕсли самый свежий снимок в
list vps— с данными, вместо ID можно написатьlatest. -
Проверьте сайт. Затем сделайте снимок вручную, чтобы локальная копия появилась сразу:
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);
- удалить с ПК старые архивы — они не зашифрованы.
- скачать снимок на ПК (
- Иногда: посмотреть
docker compose ps. Статусunhealthyуbackupозначает, что бэкапы перестали проходить (причину покажетfs-backup status).
Изменить расписание или сроки хранения. Отредактируйте BACKUP_* в .env на Pi, затем
пересоздайте контейнер:
docker compose up -d backup
Удалить именованный или pre-restore снимок. Автоматически они не удаляются, удалять нужно в обоих репозиториях:
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.
Отчёты в Telegram
Бот приложения (тот же, что для входа через Telegram) может присылать вам отчёты о бэкапах
и отвечать на команды. Пока BACKUP_TELEGRAM_CHAT_ID пуст, отчёты выключены.
Что приходит само:
- после каждого бэкапа — «✅ Бэкап»: сколько игроков и партий в БД, по каждому репозиторию снимок, число снимков и размер;
- если бэкап не удался, в том числе не начавшись (нет пароля, занята другая операция, БД не прошла проверку) — «❌ Бэкап не удался» с причиной и упавшими репозиториями;
- после еженедельной проверки — «🔍 Проверка данных: OK» или «❌».
Команды боту (отвечает только чатам из BACKUP_TELEGRAM_CHAT_ID):
/backups— хранящиеся снимки: по репозиторию число и размер, 10 последних с временем, игроками и партиями, 📌 у именованных;/status— то же, чтоfs-backup status;/help— список команд.
Настройка:
- В Telegram найдите бота приложения и напишите ему
/start. - Узнайте id своего чата:
Когда бот уже работает (id вписан), он сам забирает сообщения. Тогда id нового чата ищите в журнале:
docker compose exec backup fs-backup telegram chats # 123456789 @you /startdocker compose logs backup | grep "чужого чата". - Впишите id в
.env—BACKUP_TELEGRAM_CHAT_ID=123456789, несколько через запятую — и пересоздайте контейнер:docker compose up -d backup. - Проверьте связь:
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. Неполадки
Первое, что стоит сделать при любой проблеме:
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. Данные удалены намеренно: fs-backup run --allow-empty |
копия БД не прошла PRAGMA integrity_check |
живая БД повреждена | снимок не создаётся, старые целы. Восстановить последний хороший снимок (раздел 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. Команда сама определяет, на каком этапе оборвалось восстановление:
- если замена успела пройти во всех томах — дочищает промежуточные папки, восстановленные данные остаются;
- иначе возвращает каждому тому прежние данные.
После неё можно спокойно запускать приложение или повторить восстановление.
Пароль бэкапов потерян навсегда. Прочитать существующие снимки невозможно. Начать заново (это удалит все старые бэкапы):
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 |
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 <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_TELEGRAM_CHAT_ID |
Pi | — | id чатов для отчётов и команд, через запятую; пусто = без Telegram |
BACKUP_TELEGRAM_BOT_TOKEN |
Pi | TELEGRAM_BOT_TOKEN |
токен бота, если отчёты должен слать другой бот |
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есть строкаПоследняя проверка данных: … данные целы