Files

Бэкапы Forbidden Stars

Пошаговая инструкция: как включить бэкапы, проверить, что они работают, скачать их на ПК и восстановить данные — в том числе на новом Pi, если старый умер.

Команды даны целиком — копируйте их как есть. Где нужно подставить своё значение, это написано в угловых скобках: <IP-адрес-Pi>. Каждый шаг заканчивается блоком «Что должно получиться» — не переходите к следующему шагу, пока не получили то же самое.

Содержание

  1. Как это устроено
  2. Пароль шифрования — ПК
  3. SSH-ключ для VPS — ПК
  4. VPS: пользователь только для SFTP — VPS
  5. Сборка и публикация образов — ПК
  6. Pi: включить бэкапы — Pi
  7. ПК: доступ к Pi и выгрузка бэкапов — ПК
  8. Проверка скачанного архива — ПК
  9. Восстановление прода — Pi
  10. Катастрофа: Pi умер — новый Pi
  11. Повседневные действия
  12. Неполадки
  13. Справочник: команды и переменные
  14. Итоговый чек-лист

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 — известная программа для бэкапов. Каждую ночь контейнер:

  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.

Ограничения, о которых стоит знать

  • Копии на ПК (backups\*.tar) не зашифрованы: там данные игроков. Не выкладывайте их никуда и удаляйте ненужные.
  • Ключ, которым Pi заходит на VPS, умеет и удалять файлы в /srv/fs-backups. Если Pi будет взломан, злоумышленник сможет удалить копию на VPS. Поэтому раз в месяц полезно скачивать снимок на ПК (раздел 10).

Шаг 1. Пароль шифрования

Где: ПК. Сколько времени: 5 минут.

⚠️ Самое важное во всей инструкции

Потеряете пароль — ни один бэкап не восстановить. Ни на Pi, ни на VPS. Никакого «сброса пароля» у restic нет, и это не баг, а суть шифрования.

  1. Откройте 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('/','_')
    
  2. Сохраните результат в менеджер паролей (Bitwarden, KeePass, 1Password…), запись назовите, например, «Forbidden Stars — пароль бэкапов (restic)».

  3. Сделайте вторую копию вне компьютера: распечатайте или запишите на бумагу и уберите в надёжное место.

  4. Рекомендуется: в той же записи менеджера паролей храните копию файла .env с Pi (после шага 5). При гибели Pi это сэкономит час восстановления секретов.

Что должно получиться: строка из ~43 символов (латиница, цифры, -, _), сохранённая в менеджере паролей и на бумаге.

В пароле не должно быть символа $ — docker compose воспринимает его как переменную. Обе команды выше $ не генерируют.


Шаг 2. SSH-ключ для VPS

Где: ПК, PowerShell, папка репозитория. Зачем: этим ключом контейнер backup на Pi будет заходить на VPS. Пароль к ключу не ставим: контейнер работает без человека.

  1. Перейдите в папку репозитория (подставьте свой путь):

    cd C:\Users\<вы>\ForbidenStarsApp
    
  2. Создайте ключ:

    ssh-keygen -t ed25519 -C "fs-backup" -f deploy\backup\id_backup
    

    На вопросы Enter passphrase и Enter same passphrase again просто дважды нажмите Enter (пароль к ключу пустой).

  3. Проверьте, что появились два файла:

    Get-ChildItem deploy\backup\id_backup*
    Get-Content deploy\backup\id_backup.pub
    
  4. Проверьте, что ключ не попадёт в 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) — его трогать нельзя.

  1. Зайдите на VPS (с ПК):

    ssh root@186.246.51.17
    

    Все команды ниже выполняются на VPS от root. Заходите не под root — добавляйте sudo перед каждой командой.

  2. Убедитесь, что такого пользователя ещё нет:

    id fsbackup
    

    Ожидается id: 'fsbackup': no such user. Если пользователь уже есть — пропустите пункт 3.

  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
    
  4. Добавьте публичный ключ из шага 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.

  5. Запретите этому пользователю всё, кроме 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
    
  6. Убедитесь, что основной конфиг подключает папку 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.

  7. Проверьте конфиг и примените его. Текущая SSH-сессия при этом не оборвётся:

    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:

    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:

    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 закрыт:

    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. Один раз войдите в реестр (если уже входили — пропустите):

    docker login gitea.arseniev.info
    
  2. Соберите и опубликуйте образы (первый раз — 5–15 минут):

    .\scripts\build-push.ps1
    
  3. Проверьте, что образ бэкапа есть в реестре и собран под 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 минут.

  1. Зайдите на Pi и перейдите в папку прода:

    ssh pi@<IP-адрес-Pi>
    
    cd ~/forbidden-stars
    ls
    

    Должны быть видны docker-compose.yml и .env. Если папка называется иначе, дальше везде используйте своё название.

  2. Сохраните копию текущего compose-файла — на случай отката:

    cp docker-compose.yml docker-compose.yml.bak-$(date +%F)
    
  3. Скачайте новый 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.yml
    

    grep должен найти строку backup:. Если не нашёл, в этой ветке функционала ещё нет.

  4. Посмотрите, какие 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)? Блок ниже не дописывайте, а заполните существующие строки теми же значениями. Иначе переменные задвоятся.
  5. Откройте .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_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, не показывая секреты на экране:

    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
    
  7. Запустите только контейнер бэкапа. Сайт при этом не перезапускается:

    docker compose up -d backup
    
  8. Смотрите журнал первого запуска (выход — 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): и две строки расписания.
  9. Проверьте состояние и хронологию:

    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):

    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 без пароля:

    ssh pi@<IP-адрес-Pi> "echo ok && cd ~/forbidden-stars && docker compose ps backup"
    

    Если выводится ok и строка контейнера backup без запроса пароля — переходите к пункту 3.

  2. Если спрашивает пароль, настройте вход по ключу (один раз).

    Создайте ключ ПК, если его ещё нет. На вопросы 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 — пароль спрашиваться не должен.

  3. Добавьте в .env на ПК (корень репозитория) или исправьте, если строки уже есть:

    BACKUP_PI_SSH=pi@<IP-адрес-Pi>
    BACKUP_PI_DIR=~/forbidden-stars
    
  4. Проверьте скрипт:

    .\scripts\fs-backup.ps1 status
    .\scripts\fs-backup.ps1 list
    

    Если PowerShell пишет running scripts is disabled on this system, выполните один раз: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.

  5. Скачайте последний снимок:

    .\scripts\fs-backup.ps1 pull
    

    Скрипт делает всё сам:

    • выгружает снимок в файл на Pi;
    • копирует файл на ПК по scp;
    • сверяет контрольную сумму sha256;
    • проверяет, что внутри есть БД;
    • удаляет временный файл на Pi.
  6. Посмотрите, что внутри архива:

    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); здесь проверяется содержимое архива.

  1. Распакуйте последний скачанный архив во временную папку:

    $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 (вкладка «Выполнить SQL») и выполните:

    PRAGMA integrity_check;
    SELECT (SELECT count(*) FROM users WHERE role = 'player') AS players,
           (SELECT count(*) FROM matches) AS matches;
    
  3. Закройте DB Browser и удалите временную папку — данные в ней не зашифрованы:

    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 и выберите снимок:

    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. Заодно у вас останется копия этого снимка вне Pi.

  3. Остановите приложение. Сайт покажет страницу «Технические шоколадки»:

    docker compose stop app
    
  4. Восстановите снимок. С VPS — добавьте в конец --repo vps:

    docker compose exec backup fs-backup restore <ID> --yes
    
  5. Запустите приложение и проверьте его:

    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.

  1. С ПК скопируйте архив на Pi:

    scp backups\fs_20260914_0400_3f2a9c1d.tar pi@<IP-адрес-Pi>:~/
    
  2. На 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).
  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.
  2. Запустите всё:

    cd ~/forbidden-stars
    docker compose up -d
    
  3. Дождитесь, пока приложение создаст пустую БД (docker compose ps → app (healthy)). Контейнер backup стартует одновременно с app и сразу пробует сделать первый бэкап. В его журнале (docker compose logs backup) нормально увидеть одно из двух:

    • если backup успел раньше, чем app создал БД:

      ОШИБКА: БД /fs-db/forbidden_stars.db не найдена — приложение ещё ни разу не запускалось?
      Первый бэкап не удался — следующая попытка по расписанию.
      
    • если БД уже была:

      В БД нет ни игроков, ни партий, а последний снимок в репозитории vps — с данными.
      Похоже на новый или очищенный сервер: бэкап НЕ сделан, чтобы пустые данные не вытеснили историю.
      

      Это защита: пустой новый Pi не перезапишет историю на VPS.

  4. Посмотрите снимки на VPS и выберите нужный (обычно самый свежий с данными):

    docker compose exec backup fs-backup list vps
    

    Проверьте, что у выбранного снимка в столбцах Игроков/Партий числа, а не 0 и не ?. Защита выше не распознаёт БД без таблиц: если первый бэкап попал ровно в момент создания БД, в списке может появиться свежий снимок с ? (задача #74). Такой снимок не выбирайте.

  5. Восстановите, подставив 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.

  6. Проверьте сайт. Затем сделайте снимок вручную, чтобы локальная копия появилась сразу:

    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.

Никогда не выполняйте на проде 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
неверный 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
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 есть строка Последняя проверка данных: … данные целы