Настройка бэкапа (#64) #66
+1
-3
@@ -1,10 +1,8 @@
|
|||||||
# ── Нормализация переводов строк ──────────────────────────────────────────────
|
# ── Нормализация переводов строк ──────────────────────────────────────────────
|
||||||
# Шелл-скрипты обязаны быть с LF: в Linux-контейнере и на Pi CRLF ломает shebang.
|
# Шелл-скрипты обязаны быть с LF: в Linux-контейнере и на Pi CRLF ломает shebang.
|
||||||
# entrypoint.sh контейнер чинит сам (sed в Dockerfile), но backup.sh запускается
|
# Скрипты образов (entrypoint.sh, deploy/*/…) контейнер дополнительно чинит sed-ом при сборке.
|
||||||
# с хоста Pi — для него LF в репозитории критичен.
|
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
backend/entrypoint.sh text eol=lf
|
backend/entrypoint.sh text eol=lf
|
||||||
scripts/backup.sh text eol=lf
|
|
||||||
|
|
||||||
# ── export-ignore: НЕ попадает в `git archive` (чистая выгрузка прод/тест) ─────
|
# ── export-ignore: НЕ попадает в `git archive` (чистая выгрузка прод/тест) ─────
|
||||||
# В git эти файлы есть и доступны на всех ветках (нужны для разработки),
|
# В git эти файлы есть и доступны на всех ветках (нужны для разработки),
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ frontend/.vite/
|
|||||||
# Сгенерированный снапшот OpenAPI (контракт фронта закоммичен в schema.d.ts)
|
# Сгенерированный снапшот OpenAPI (контракт фронта закоммичен в schema.d.ts)
|
||||||
backend/openapi.json
|
backend/openapi.json
|
||||||
|
|
||||||
# Бэкапы (создаёт scripts/backup.sh на Pi)
|
# Бэкапы, скачанные на ПК (scripts/fs-backup.* pull) — не зашифрованы, в git не идут
|
||||||
backups/
|
backups/
|
||||||
|
|
||||||
# Фронт-макеты для проработки UI (локальные прототипы, не для репозитория)
|
# Фронт-макеты для проработки UI (локальные прототипы, не для репозитория)
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ HTTPS твоими сертификатами и проксирует трафи
|
|||||||
2. **Pi (прод)** — [`pi/README.md`](pi/README.md): ключ в `deploy/tunnel/id_tunnel`, `.env`, `docker compose up -d`.
|
2. **Pi (прод)** — [`pi/README.md`](pi/README.md): ключ в `deploy/tunnel/id_tunnel`, `.env`, `docker compose up -d`.
|
||||||
3. **ПК (dev/test)** — тот же ключ в `deploy/tunnel/id_tunnel` (для test-контейнера) и/или
|
3. **ПК (dev/test)** — тот же ключ в `deploy/tunnel/id_tunnel` (для test-контейнера) и/или
|
||||||
ключ по умолчанию для dev (`run.ps1`); pubkey — в `authorized_keys` у `tunnel@VPS`.
|
ключ по умолчанию для dev (`run.ps1`); pubkey — в `authorized_keys` у `tunnel@VPS`.
|
||||||
|
4. **Бэкапы** — [`backup/README.md`](backup/README.md): контейнер `backup` (restic) делает
|
||||||
|
снимки на Pi и на VPS (`fsbackup@VPS`, только SFTP), скрипты ПК скачивают их и проверяют
|
||||||
|
восстановление на тест-клоне.
|
||||||
|
|
||||||
Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, `id_tunnel*`) живут на VPS/Pi/ПК,
|
Секреты не в git: сертификаты/ключи (`*.pem`, `*.key`, `id_tunnel*`) живут на VPS/Pi/ПК,
|
||||||
в репозитории только `Caddyfile`, `deploy/tunnel/` (образ туннеля) и шаблоны.
|
в репозитории только `Caddyfile`, `deploy/tunnel/` (образ туннеля) и шаблоны.
|
||||||
|
|||||||
@@ -0,0 +1,980 @@
|
|||||||
|
# Бэкапы Forbidden Stars
|
||||||
|
|
||||||
|
Пошаговая инструкция: как включить бэкапы, проверить, что они работают, скачать их на ПК
|
||||||
|
и восстановить данные — в том числе на новом Pi, если старый умер.
|
||||||
|
|
||||||
|
Команды даны целиком — копируйте их как есть. Где нужно подставить своё значение, это
|
||||||
|
написано в угловых скобках: `<IP-адрес-Pi>`. Каждый шаг заканчивается блоком
|
||||||
|
**«Что должно получиться»** — не переходите к следующему шагу, пока не получили то же самое.
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
|
||||||
|
0. [Как это устроено](#0-как-это-устроено)
|
||||||
|
1. [Пароль шифрования](#шаг-1-пароль-шифрования) — ПК
|
||||||
|
2. [SSH-ключ для VPS](#шаг-2-ssh-ключ-для-vps) — ПК
|
||||||
|
3. [VPS: пользователь только для SFTP](#шаг-3-vps-пользователь-только-для-sftp) — VPS
|
||||||
|
4. [Сборка и публикация образов](#шаг-4-сборка-и-публикация-образов) — ПК
|
||||||
|
5. [Pi: включить бэкапы](#шаг-5-pi-включить-бэкапы) — Pi
|
||||||
|
6. [ПК: доступ к Pi и выгрузка бэкапов](#шаг-6-пк-доступ-к-pi-и-выгрузка-бэкапов) — ПК
|
||||||
|
7. [Учебное восстановление на тест-клоне](#шаг-7-учебное-восстановление-на-тест-клоне) — ПК
|
||||||
|
8. [Восстановление прода](#8-восстановление-прода) — Pi
|
||||||
|
9. [Катастрофа: Pi умер](#9-катастрофа-pi-умер) — новый Pi
|
||||||
|
10. [Повседневные действия](#10-повседневные-действия)
|
||||||
|
11. [Неполадки](#11-неполадки)
|
||||||
|
12. [Справочник: команды и переменные](#12-справочник-команды-и-переменные)
|
||||||
|
13. [Итоговый чек-лист](#13-итоговый-чек-лист)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Как это устроено
|
||||||
|
|
||||||
|
```
|
||||||
|
Raspberry Pi (прод) VPS 186.246.51.17
|
||||||
|
┌──────────────────────────────────────────┐ ┌─────────────────────────────┐
|
||||||
|
│ app ──► тома: БД, uploads, achievements │ │ /srv/fs-backups/restic │
|
||||||
|
│ ▲ читает (консистентно) │ SFTP │ (копия №2, зашифрована) │
|
||||||
|
│ backup ──────┘ │ ────────────► │ пользователь fsbackup: │
|
||||||
|
│ │ каждую ночь в 04:00 │ (Pi сам │ только SFTP, без shell │
|
||||||
|
│ ▼ │ ходит └─────────────────────────────┘
|
||||||
|
│ том backup-data (копия №1, зашифрована) │ наружу)
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
▲ ssh + scp по команде «pull»
|
||||||
|
┌─────────┴────────────────────────────────┐
|
||||||
|
│ ПК: backups\fs_<дата>_<id>.tar │ копия №3, по запросу, НЕ зашифрована
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
На Pi работает третий контейнер — `backup`. В нём [restic](https://restic.net) — известная
|
||||||
|
программа для бэкапов. Каждую ночь контейнер:
|
||||||
|
|
||||||
|
1. снимает **консистентную** копию БД. Сайт при этом работает, пользователи ничего не замечают;
|
||||||
|
2. проверяет копию (`PRAGMA integrity_check`). Битая копия не сохраняется, старые снимки
|
||||||
|
тоже не трогаются;
|
||||||
|
3. сохраняет **снимок** — БД + загруженные фото (`uploads`) + титулы (`achievements`) — в
|
||||||
|
два места: на сам Pi и на VPS;
|
||||||
|
4. удаляет старые снимки по правилам хранения;
|
||||||
|
5. проверяет, что репозитории целы.
|
||||||
|
|
||||||
|
Раз в неделю (воскресенье, 05:30) дополнительно читает часть сохранённых данных и убеждается,
|
||||||
|
что последний снимок действительно восстанавливается.
|
||||||
|
|
||||||
|
**Словарь**
|
||||||
|
|
||||||
|
| Слово | Что значит |
|
||||||
|
|---|---|
|
||||||
|
| **Снимок** (snapshot) | Состояние данных на момент бэкапа. У каждого есть короткий ID, например `3f2a9c1d`. |
|
||||||
|
| **Репозиторий** | Хранилище снимков. У нас их два: `local` (на Pi) и `vps` (на VPS). |
|
||||||
|
| **Пароль шифрования** | Им зашифрованы оба репозитория. Без него снимки прочитать нельзя — **никак**. |
|
||||||
|
| **Хранение** | Сколько снимков остаётся: последний за каждый из 14 дней, за каждую из 8 недель, за каждый из 12 месяцев, плюс 3 самых свежих. Одинаковые данные хранятся один раз (дедупликация), поэтому 30+ снимков занимают немногим больше одного. |
|
||||||
|
| **Именованный снимок** | Снимок, сделанный вручную с меткой (`--tag before-update`). Автоматически не удаляется. |
|
||||||
|
| **pre-restore** | Страховочный снимок, который автоматически делается перед каждым восстановлением: «как было до». Автоматически не удаляется. |
|
||||||
|
|
||||||
|
**Что попадает в снимок:** БД, `uploads`, `achievements`.
|
||||||
|
**Что НЕ попадает:** файл `.env` с секретами (пароли, токен бота, ключи). Его копию храните
|
||||||
|
отдельно — см. [шаг 1](#шаг-1-пароль-шифрования).
|
||||||
|
|
||||||
|
**Ограничения, о которых стоит знать**
|
||||||
|
|
||||||
|
- Копии на ПК (`backups\*.tar`) **не зашифрованы**: там данные игроков. Не выкладывайте их
|
||||||
|
никуда и удаляйте ненужные.
|
||||||
|
- Ключ, которым Pi заходит на VPS, умеет и удалять файлы в `/srv/fs-backups`. Если Pi будет
|
||||||
|
взломан, злоумышленник сможет удалить копию на VPS. Поэтому раз в месяц полезно скачивать
|
||||||
|
снимок на ПК ([раздел 10](#10-повседневные-действия)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 1. Пароль шифрования
|
||||||
|
|
||||||
|
**Где:** ПК. **Сколько времени:** 5 минут.
|
||||||
|
|
||||||
|
> ### ⚠️ Самое важное во всей инструкции
|
||||||
|
> Потеряете пароль — **ни один бэкап не восстановить**. Ни на Pi, ни на VPS. Никакого
|
||||||
|
> «сброса пароля» у restic нет, и это не баг, а суть шифрования.
|
||||||
|
|
||||||
|
1. Откройте PowerShell и сгенерируйте пароль:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `python` не найден, подойдёт такая команда:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$b = New-Object byte[] 32; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); [Convert]::ToBase64String($b).TrimEnd('=').Replace('+','-').Replace('/','_')
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Сохраните результат в менеджер паролей (Bitwarden, KeePass, 1Password…), запись
|
||||||
|
назовите, например, **«Forbidden Stars — пароль бэкапов (restic)»**.
|
||||||
|
3. Сделайте вторую копию вне компьютера: распечатайте или запишите на бумагу и уберите
|
||||||
|
в надёжное место.
|
||||||
|
4. **Рекомендуется:** в той же записи менеджера паролей храните копию файла `.env` с Pi
|
||||||
|
(после шага 5). При гибели Pi это сэкономит час восстановления секретов.
|
||||||
|
|
||||||
|
**Что должно получиться:** строка из ~43 символов (латиница, цифры, `-`, `_`), сохранённая
|
||||||
|
в менеджере паролей и на бумаге.
|
||||||
|
|
||||||
|
> В пароле не должно быть символа `$` — docker compose воспринимает его как переменную.
|
||||||
|
> Обе команды выше `$` не генерируют.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 2. SSH-ключ для VPS
|
||||||
|
|
||||||
|
**Где:** ПК, PowerShell, папка репозитория. **Зачем:** этим ключом контейнер `backup`
|
||||||
|
на Pi будет заходить на VPS. Пароль к ключу не ставим: контейнер работает без человека.
|
||||||
|
|
||||||
|
1. Перейдите в папку репозитория (подставьте свой путь):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\<вы>\ForbidenStarsApp
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Создайте ключ:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh-keygen -t ed25519 -C "fs-backup" -f deploy\backup\id_backup
|
||||||
|
```
|
||||||
|
|
||||||
|
На вопросы `Enter passphrase` и `Enter same passphrase again` просто **дважды нажмите
|
||||||
|
Enter** (пароль к ключу пустой).
|
||||||
|
|
||||||
|
3. Проверьте, что появились два файла:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-ChildItem deploy\backup\id_backup*
|
||||||
|
Get-Content deploy\backup\id_backup.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Проверьте, что ключ не попадёт в git (команда должна вывести **пустоту**):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git status --short deploy\backup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что должно получиться:**
|
||||||
|
- файлы `id_backup` (приватный, секрет) и `id_backup.pub` (публичный);
|
||||||
|
- содержимое `id_backup.pub` — одна строка вида `ssh-ed25519 AAAAC3Nza…много символов… fs-backup`;
|
||||||
|
- `git status` по этой папке ничего не показывает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 3. VPS: пользователь только для SFTP
|
||||||
|
|
||||||
|
**Где:** VPS. **Зачем:** отдельный пользователь `fsbackup` сможет только класть и читать
|
||||||
|
файлы бэкапов по SFTP: без shell, без туннелей, без входа по паролю.
|
||||||
|
|
||||||
|
> Почему `fsbackup`, а не `backup`: в Debian/Ubuntu системный пользователь `backup` уже
|
||||||
|
> существует (он служебный, домашняя папка `/var/backups`) — его трогать нельзя.
|
||||||
|
|
||||||
|
1. Зайдите на VPS (с ПК):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh root@186.246.51.17
|
||||||
|
```
|
||||||
|
|
||||||
|
Все команды ниже выполняются **на VPS** от root. Заходите не под root — добавляйте
|
||||||
|
`sudo` перед каждой командой.
|
||||||
|
|
||||||
|
2. Убедитесь, что такого пользователя ещё нет:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
id fsbackup
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается `id: 'fsbackup': no such user`. Если пользователь уже есть — пропустите пункт 3.
|
||||||
|
|
||||||
|
3. Создайте пользователя и папки:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
useradd --create-home --shell /usr/sbin/nologin fsbackup
|
||||||
|
install -d -m 700 -o fsbackup -g fsbackup /home/fsbackup/.ssh
|
||||||
|
install -m 600 -o fsbackup -g fsbackup /dev/null /home/fsbackup/.ssh/authorized_keys
|
||||||
|
install -d -m 700 -o fsbackup -g fsbackup /srv/fs-backups
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Добавьте публичный ключ из шага 2.
|
||||||
|
- **На ПК**, во втором окне PowerShell в папке репозитория, скопируйте ключ в буфер обмена:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-Content deploy\backup\id_backup.pub | Set-Clipboard
|
||||||
|
```
|
||||||
|
|
||||||
|
- **На VPS** наберите команду ниже, вставив ключ вместо `ВСТАВЬТЕ_КЛЮЧ`. Вставка в
|
||||||
|
терминале — правая кнопка мыши или `Ctrl+Shift+V`. Одинарные кавычки оставьте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 'ВСТАВЬТЕ_КЛЮЧ' >> /home/fsbackup/.ssh/authorized_keys
|
||||||
|
cat /home/fsbackup/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
`cat` должен показать одну строку, которая начинается с `ssh-ed25519` и заканчивается на `fs-backup`.
|
||||||
|
|
||||||
|
5. Запретите этому пользователю всё, кроме SFTP. Скопируйте блок **целиком**, от `cat` до
|
||||||
|
последнего `EOF` включительно:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > /etc/ssh/sshd_config.d/60-fs-backup.conf <<'EOF'
|
||||||
|
Match User fsbackup
|
||||||
|
ForceCommand internal-sftp -d /srv/fs-backups
|
||||||
|
PasswordAuthentication no
|
||||||
|
AllowTcpForwarding no
|
||||||
|
AllowAgentForwarding no
|
||||||
|
PermitTunnel no
|
||||||
|
X11Forwarding no
|
||||||
|
PermitTTY no
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Убедитесь, что основной конфиг подключает папку `sshd_config.d`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n '^Include' /etc/ssh/sshd_config
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается строка `Include /etc/ssh/sshd_config.d/*.conf`. **Если вывода нет** (старая
|
||||||
|
система), допишите блок в конец основного конфига:
|
||||||
|
`cat /etc/ssh/sshd_config.d/60-fs-backup.conf >> /etc/ssh/sshd_config`.
|
||||||
|
|
||||||
|
7. Проверьте конфиг и примените его. Текущая SSH-сессия при этом не оборвётся:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sshd -t && echo "конфиг OK"
|
||||||
|
systemctl reload ssh || systemctl reload sshd
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `sshd -t` вывел ошибку, **не выходите из сессии** и исправьте файл
|
||||||
|
(`nano /etc/ssh/sshd_config.d/60-fs-backup.conf`). Ошибка в конфиге sshd может закрыть
|
||||||
|
вход на сервер.
|
||||||
|
|
||||||
|
8. Проверьте, что правила действуют только на `fsbackup`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sshd -T -C user=fsbackup,host=x,addr=1.2.3.4 | grep -i forcecommand
|
||||||
|
sshd -T -C user=root,host=x,addr=1.2.3.4 | grep -i forcecommand
|
||||||
|
```
|
||||||
|
|
||||||
|
Первая команда должна показать `forcecommand internal-sftp -d /srv/fs-backups`,
|
||||||
|
вторая — `forcecommand none`.
|
||||||
|
|
||||||
|
9. **С ПК** (новое окно PowerShell в папке репозитория) проверьте вход по SFTP:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
sftp -i deploy\backup\id_backup fsbackup@186.246.51.17
|
||||||
|
```
|
||||||
|
|
||||||
|
При первом подключении ответьте `yes` на вопрос `Are you sure you want to continue
|
||||||
|
connecting`. Появится приглашение `sftp>`. Наберите `pwd`, затем `bye`.
|
||||||
|
|
||||||
|
10. **С ПК** проверьте, что shell закрыт:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh -i deploy\backup\id_backup fsbackup@186.246.51.17
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что должно получиться:**
|
||||||
|
- `sshd -t` → `конфиг OK`;
|
||||||
|
- в `sftp` команда `pwd` отвечает `Remote working directory: /srv/fs-backups`;
|
||||||
|
- `ssh` из пункта 10 отвечает `This service allows sftp connections only.` и сразу отключается;
|
||||||
|
- вход root на VPS работает как раньше (проверьте новым окном: `ssh root@186.246.51.17`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 4. Сборка и публикация образов
|
||||||
|
|
||||||
|
**Где:** ПК с Docker Desktop, папка репозитория. **Зачем:** на Pi нет сборки — он скачивает
|
||||||
|
готовые образы из реестра Gitea. Новый образ `forbidden-stars-backup` нужно туда положить.
|
||||||
|
|
||||||
|
> `build-push` собирает **все три** образа (app, tunnel, backup) из текущей ветки ПК и
|
||||||
|
> публикует их с тегом из `IMAGE_TAG`. Убедитесь, что вы на ветке, которая должна быть на
|
||||||
|
> проде (обычно `main` после релиза): `git branch --show-current`.
|
||||||
|
|
||||||
|
1. Один раз войдите в реестр (если уже входили — пропустите):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker login gitea.arseniev.info
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Соберите и опубликуйте образы (первый раз — 5–15 минут):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\build-push.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Проверьте, что образ бэкапа есть в реестре и собран под arm64:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker buildx imagetools inspect gitea.arseniev.info/notbigghost/forbidden-stars-backup:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что должно получиться:**
|
||||||
|
- `build-push.ps1` заканчивается зелёной строкой `Done. On the Pi: ...`;
|
||||||
|
- `imagetools inspect` показывает `Platform: linux/arm64`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 5. Pi: включить бэкапы
|
||||||
|
|
||||||
|
**Где:** Pi. **Сколько времени:** 15 минут.
|
||||||
|
|
||||||
|
1. Зайдите на Pi и перейдите в папку прода:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh pi@<IP-адрес-Pi>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/forbidden-stars
|
||||||
|
ls
|
||||||
|
```
|
||||||
|
|
||||||
|
Должны быть видны `docker-compose.yml` и `.env`. Если папка называется иначе, дальше
|
||||||
|
везде используйте своё название.
|
||||||
|
|
||||||
|
2. Сохраните копию текущего compose-файла — на случай отката:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp docker-compose.yml docker-compose.yml.bak-$(date +%F)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Скачайте новый `docker-compose.yml`. Замените `main` на `dev`, если функционал ещё не
|
||||||
|
попал в релиз:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BRANCH=main
|
||||||
|
curl -fsSLO https://gitea.arseniev.info/NotBigGhost/ForbiddenStarsApp/raw/branch/$BRANCH/docker-compose.yml
|
||||||
|
grep -n '^ backup:' docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
`grep` должен найти строку ` backup:`. Если не нашёл, в этой ветке функционала ещё нет.
|
||||||
|
|
||||||
|
4. Посмотрите, какие `BACKUP_*` уже есть в `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -n '^BACKUP_' .env
|
||||||
|
```
|
||||||
|
|
||||||
|
- Есть **старые** строки (`BACKUP_VPS_USER=backup`, `BACKUP_VPS_DIR=/srv/fs-backups`,
|
||||||
|
`BACKUP_VPS_KEY`, `BACKUP_KEEP_LOCAL`, `BACKUP_KEEP_REMOTE`)? **Удалите их все** в
|
||||||
|
пункте 5: они указывают на неправильного пользователя и папку.
|
||||||
|
- Уже есть **новые** строки (`BACKUP_PASSWORD`, `BACKUP_SSH_KEY_B64` и т.д. — если `.env`
|
||||||
|
делался из свежего `.env.example`)? Блок ниже **не дописывайте**, а заполните
|
||||||
|
существующие строки теми же значениями. Иначе переменные задвоятся.
|
||||||
|
|
||||||
|
5. Откройте `.env` и добавьте в конец блок ниже:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
В nano: стрелками вниз до конца файла, вставка — правая кнопка мыши, сохранить —
|
||||||
|
`Ctrl+O`, затем `Enter`, выйти — `Ctrl+X`.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# ─── БЭКАПЫ ───
|
||||||
|
BACKUP_PASSWORD=<пароль из шага 1>
|
||||||
|
BACKUP_SCHEDULE="0 4 * * *"
|
||||||
|
BACKUP_VERIFY_SCHEDULE="30 5 * * 0"
|
||||||
|
BACKUP_TZ=Europe/Moscow
|
||||||
|
BACKUP_KEEP_DAILY=14
|
||||||
|
BACKUP_KEEP_WEEKLY=8
|
||||||
|
BACKUP_KEEP_MONTHLY=12
|
||||||
|
BACKUP_COMPRESSION=max
|
||||||
|
BACKUP_MAX_AGE_HOURS=30
|
||||||
|
BACKUP_VPS_HOST=186.246.51.17
|
||||||
|
BACKUP_VPS_USER=fsbackup
|
||||||
|
BACKUP_VPS_PORT=22
|
||||||
|
BACKUP_VPS_DIR=/srv/fs-backups/restic
|
||||||
|
BACKUP_SSH_KEY_B64=<длинная строка base64, см. ниже>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Как получить `BACKUP_SSH_KEY_B64`.** На ПК, в PowerShell в папке репозитория:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
[Convert]::ToBase64String([IO.File]::ReadAllBytes((Resolve-Path "deploy\backup\id_backup"))) | Set-Clipboard
|
||||||
|
[Text.Encoding]::ASCII.GetString([Convert]::FromBase64String((Get-Clipboard))).Split("`n")[0]
|
||||||
|
```
|
||||||
|
|
||||||
|
Вторая команда проверяет содержимое буфера. Она должна вывести
|
||||||
|
`-----BEGIN OPENSSH PRIVATE KEY-----`. Теперь вставьте строку из буфера в `.env` после
|
||||||
|
`BACKUP_SSH_KEY_B64=`: **одной строкой, без пробелов и кавычек**.
|
||||||
|
|
||||||
|
**Что означает каждая строка**
|
||||||
|
|
||||||
|
| Переменная | Значение | Что делает |
|
||||||
|
|---|---|---|
|
||||||
|
| `BACKUP_PASSWORD` | пароль из шага 1 | шифрует оба репозитория; пусто = бэкапы выключены |
|
||||||
|
| `BACKUP_SCHEDULE` | `"0 4 * * *"` | когда делать бэкап: минута, час, день, месяц, день недели → каждый день в 04:00 |
|
||||||
|
| `BACKUP_VERIFY_SCHEDULE` | `"30 5 * * 0"` | когда проверять данные: воскресенье 05:30 |
|
||||||
|
| `BACKUP_TZ` | `Europe/Moscow` | часовой пояс расписания и времени снимков |
|
||||||
|
| `BACKUP_KEEP_DAILY/WEEKLY/MONTHLY` | `14` / `8` / `12` | сколько дней / недель / месяцев хранить снимки |
|
||||||
|
| `BACKUP_COMPRESSION` | `max` | сжатие restic: `auto` — быстрее, `max` — плотнее, `off` — без сжатия |
|
||||||
|
| `BACKUP_MAX_AGE_HOURS` | `30` | если успешного бэкапа не было дольше — контейнер помечается `unhealthy` |
|
||||||
|
| `BACKUP_VPS_HOST` | `186.246.51.17` | адрес VPS; пусто = копия только на Pi |
|
||||||
|
| `BACKUP_VPS_USER` | `fsbackup` | пользователь из шага 3 |
|
||||||
|
| `BACKUP_VPS_PORT` | `22` | SSH-порт VPS |
|
||||||
|
| `BACKUP_VPS_DIR` | `/srv/fs-backups/restic` | папка репозитория на VPS (создастся сама) |
|
||||||
|
| `BACKUP_SSH_KEY_B64` | base64 ключа | приватный ключ из шага 2 одной строкой |
|
||||||
|
|
||||||
|
6. Проверьте `.env`, **не показывая секреты на экране**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -c '^BACKUP_' .env
|
||||||
|
grep '^BACKUP_SSH_KEY_B64=' .env | wc -c
|
||||||
|
grep '^BACKUP_PASSWORD=' .env | grep -c '\$'
|
||||||
|
docker compose config --quiet && echo "compose OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается:
|
||||||
|
- `14` (или `16`, если в `.env` есть ещё `BACKUP_PI_SSH` и `BACKUP_PI_DIR` из нового шаблона —
|
||||||
|
на Pi они не используются и не мешают);
|
||||||
|
- число больше `400`;
|
||||||
|
- `0`;
|
||||||
|
- `compose OK`.
|
||||||
|
|
||||||
|
Проверить, что ни одна переменная не задвоилась (вывод должен быть **пустым**):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -o '^BACKUP_[A-Z_0-9]*' .env | sort | uniq -d
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Запустите **только** контейнер бэкапа. Сайт при этом не перезапускается:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d backup
|
||||||
|
```
|
||||||
|
|
||||||
|
8. Смотрите журнал первого запуска (выход — `Ctrl+C`, контейнер продолжит работать):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f backup
|
||||||
|
```
|
||||||
|
|
||||||
|
Первый бэкап начинается сразу. В журнале должны появиться, в таком порядке:
|
||||||
|
- `Снимков в локальном репозитории ещё нет — делаю первый бэкап сразу.`
|
||||||
|
- `БД в порядке: игроков N, партий M.`
|
||||||
|
- `Репозиторий local ещё не создан — создаю …`
|
||||||
|
- `OK: репозиторий local, снимок xxxxxxxx.`
|
||||||
|
- `Репозиторий vps ещё не создан — создаю (sftp:fs-vps:/srv/fs-backups/restic …`
|
||||||
|
- `OK: репозиторий vps, снимок yyyyyyyy.`
|
||||||
|
- `Бэкап завершён.`
|
||||||
|
- `Расписание (TZ=Europe/Moscow):` и две строки расписания.
|
||||||
|
|
||||||
|
9. Проверьте состояние и хронологию:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec backup fs-backup status
|
||||||
|
docker compose exec backup fs-backup list
|
||||||
|
docker compose exec backup fs-backup list vps
|
||||||
|
docker compose ps backup
|
||||||
|
```
|
||||||
|
|
||||||
|
10. На VPS убедитесь, что копия пришла (там только зашифрованные файлы restic):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la /srv/fs-backups/restic
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что должно получиться:**
|
||||||
|
- в `status` у `[local]` и `[vps]` есть строка `Последний бэкап: <сегодня> — снимок …`;
|
||||||
|
- `list` и `list vps` показывают по одному снимку с верным числом игроков и партий;
|
||||||
|
- на VPS в `/srv/fs-backups/restic` лежат `config`, `data`, `index`, `keys`, `snapshots`;
|
||||||
|
- `docker compose ps backup` показывает `Up`. Первые ~10 минут статус `(health: starting)`,
|
||||||
|
затем `(healthy)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 6. ПК: доступ к Pi и выгрузка бэкапов
|
||||||
|
|
||||||
|
**Где:** ПК. **Зачем:** одной командой скачивать снимок с Pi на ПК — третья копия, которая
|
||||||
|
не зависит ни от Pi, ни от VPS.
|
||||||
|
|
||||||
|
1. Проверьте вход на Pi **без пароля**:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh pi@<IP-адрес-Pi> "echo ok && cd ~/forbidden-stars && docker compose ps backup"
|
||||||
|
```
|
||||||
|
|
||||||
|
Если выводится `ok` и строка контейнера `backup` без запроса пароля — переходите
|
||||||
|
к пункту 3.
|
||||||
|
|
||||||
|
2. Если спрашивает пароль, настройте вход по ключу (один раз).
|
||||||
|
|
||||||
|
Создайте ключ ПК, если его ещё нет. На вопросы `passphrase` дважды нажмите Enter:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
if (-not (Test-Path "$env:USERPROFILE\.ssh\id_ed25519")) { ssh-keygen -t ed25519 -f "$env:USERPROFILE\.ssh\id_ed25519" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Передайте публичный ключ на Pi (пароль Pi спросят в последний раз):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" | ssh pi@<IP-адрес-Pi> "mkdir -p ~/.ssh && chmod 700 ~/.ssh && tr -d '\r' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
|
||||||
|
```
|
||||||
|
|
||||||
|
Повторите пункт 1 — пароль спрашиваться не должен.
|
||||||
|
|
||||||
|
3. Добавьте в `.env` **на ПК** (корень репозитория) или исправьте, если строки уже есть:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
BACKUP_PI_SSH=pi@<IP-адрес-Pi>
|
||||||
|
BACKUP_PI_DIR=~/forbidden-stars
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Проверьте скрипт:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\fs-backup.ps1 status
|
||||||
|
.\scripts\fs-backup.ps1 list
|
||||||
|
```
|
||||||
|
|
||||||
|
Если PowerShell пишет `running scripts is disabled on this system`, выполните один раз:
|
||||||
|
`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`.
|
||||||
|
|
||||||
|
5. Скачайте последний снимок:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\fs-backup.ps1 pull
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт делает всё сам:
|
||||||
|
- выгружает снимок в файл на Pi;
|
||||||
|
- копирует файл на ПК по `scp`;
|
||||||
|
- сверяет контрольную сумму sha256;
|
||||||
|
- проверяет, что внутри есть БД;
|
||||||
|
- удаляет временный файл на Pi.
|
||||||
|
|
||||||
|
6. Посмотрите, что внутри архива:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-ChildItem backups\fs_*.tar
|
||||||
|
tar -tf (Get-ChildItem backups\fs_*.tar | Sort-Object LastWriteTime | Select-Object -Last 1).FullName | Select-Object -First 10
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что должно получиться:**
|
||||||
|
- `pull` заканчивается зелёной строкой `OK: ...\backups\fs_<дата>_<время>_<id>.tar (… MB, N files, sha256 verified)`;
|
||||||
|
- внутри архива есть `forbidden_stars.db`, `uploads/…`, `achievements/…`.
|
||||||
|
|
||||||
|
> Архив — обычный `.tar`, его открывает 7-Zip. БД внутри — файл SQLite, его можно
|
||||||
|
> посмотреть в [DB Browser for SQLite](https://sqlitebrowser.org). Архив **не зашифрован**.
|
||||||
|
>
|
||||||
|
> Другие варианты: снимок с VPS — `.\scripts\fs-backup.ps1 pull -Repo vps`,
|
||||||
|
> конкретный снимок — `.\scripts\fs-backup.ps1 pull -Snapshot 3f2a9c1d` (ID из `list`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 7. Учебное восстановление на тест-клоне
|
||||||
|
|
||||||
|
**Где:** ПК с Docker Desktop. **Зачем:** убедиться, что бэкап действительно
|
||||||
|
восстанавливается, **до** того как это понадобится по-настоящему. Прод не затрагивается.
|
||||||
|
|
||||||
|
> Данные тест-клона на ПК будут заменены данными из архива. Прежние данные тест-клона
|
||||||
|
> сохраняются в его собственный снимок `pre-restore`.
|
||||||
|
|
||||||
|
1. Восстановите скачанный архив в тест-клон (подставьте имя своего файла):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\fs-backup.ps1 restore-test -File backups\fs_20260914_0400_3f2a9c1d.tar
|
||||||
|
```
|
||||||
|
|
||||||
|
В первый раз Docker соберёт образы тест-клона — это несколько минут.
|
||||||
|
|
||||||
|
2. Проверьте, что приложение тест-клона поднялось:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f docker-compose.test.yml ps
|
||||||
|
docker compose -f docker-compose.test.yml logs --tail 20 app
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Посмотрите на сайт: в `.env` на ПК временно поставьте `APP_ENV=test` и запустите
|
||||||
|
`.\run.ps1`. Тест-клон откроется на `https://forbidden-stars.ru`: проверьте топ,
|
||||||
|
профили, историю партий. Потом верните `APP_ENV=development`.
|
||||||
|
|
||||||
|
**Что должно получиться:**
|
||||||
|
- в выводе `restore-test`:
|
||||||
|
- `Развёрнутые данные в порядке: игроков N, партий M.` — те же числа, что в `list` на Pi;
|
||||||
|
- `Данные восстановлены.`;
|
||||||
|
- `Done. The test clone now runs on the restored data.`;
|
||||||
|
- `docker compose ... ps` показывает `app` в состоянии `Up … (healthy)`;
|
||||||
|
- на сайте тест-клона — данные прода на момент снимка.
|
||||||
|
|
||||||
|
> Этим же способом можно восстановить в тест-клон старые архивы `fs_*.tar.gz` прежнего
|
||||||
|
> `scripts/backup.sh`: `.\scripts\fs-backup.ps1 restore-test -File backups\fs_20260710_140914.tar.gz`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` — страховочный, прочие — имя, данное вручную |
|
||||||
|
|
||||||
|
Если локальный репозиторий повреждён или пуст, смотрите копию на VPS:
|
||||||
|
`docker compose exec backup fs-backup list vps`.
|
||||||
|
|
||||||
|
2. **По желанию, но рекомендуется:** сначала отрепетируйте на ПК:
|
||||||
|
`.\scripts\fs-backup.ps1 pull -Snapshot <ID>`, затем `restore-test` ([шаг 7](#шаг-7-учебное-восстановление-на-тест-клоне)).
|
||||||
|
|
||||||
|
3. Остановите приложение. Сайт покажет страницу «Технические шоколадки»:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose stop app
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Восстановите снимок. С VPS — добавьте в конец `--repo vps`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec backup fs-backup restore <ID> --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Запустите приложение и проверьте его:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose start app
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Через 30–60 секунд у `app` должно быть `(healthy)`. Откройте `https://forbiddenstars.ru`.
|
||||||
|
|
||||||
|
**Что должно получиться:** в выводе `restore` — `Развёрнутые данные в порядке: …`,
|
||||||
|
`Страховочный снимок: <id>`, `Данные восстановлены.`; сайт работает на данных из снимка.
|
||||||
|
|
||||||
|
**Если что-то пошло не так**
|
||||||
|
|
||||||
|
- `restore` закончился строкой `ОШИБКА: … текущие данные НЕ тронуты` — данные прежние,
|
||||||
|
просто запустите приложение (`docker compose start app`) и разберитесь с причиной
|
||||||
|
([раздел 11](#11-неполадки)).
|
||||||
|
- Восстановили не тот снимок — верните «как было до»: в `list` найдите самый свежий снимок
|
||||||
|
с меткой `pre-restore` и восстановите его так же (пункты 3–5).
|
||||||
|
- После старта сайт показывает заглушку дольше пары минут: `docker compose logs --tail 50 app`,
|
||||||
|
затем `docker compose restart tunnel`.
|
||||||
|
|
||||||
|
### Восстановление из файла-архива
|
||||||
|
|
||||||
|
Например, из архива, скачанного на ПК, или из старого `fs_*.tar.gz`.
|
||||||
|
|
||||||
|
1. С ПК скопируйте архив на Pi:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scp backups\fs_20260914_0400_3f2a9c1d.tar pi@<IP-адрес-Pi>:~/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. На Pi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/forbidden-stars
|
||||||
|
docker compose cp ~/fs_20260914_0400_3f2a9c1d.tar backup:/import/fs.tar
|
||||||
|
docker compose stop app
|
||||||
|
docker compose exec backup fs-backup import /import/fs.tar --yes
|
||||||
|
docker compose exec backup rm /import/fs.tar
|
||||||
|
docker compose start app
|
||||||
|
rm ~/fs_20260914_0400_3f2a9c1d.tar
|
||||||
|
```
|
||||||
|
|
||||||
|
`import` проходит те же проверки и так же делает снимок `pre-restore`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Катастрофа: Pi умер
|
||||||
|
|
||||||
|
**Когда:** Pi сгорел, SD-карта испорчена, Pi украли. Копия на Pi потеряна, остаётся VPS
|
||||||
|
(и архивы на ПК).
|
||||||
|
|
||||||
|
**Что понадобится:**
|
||||||
|
- пароль бэкапов из менеджера паролей — **без него дальше идти бессмысленно**;
|
||||||
|
- копия `.env` старого Pi (если сохраняли, [шаг 1](#шаг-1-пароль-шифрования)).
|
||||||
|
|
||||||
|
1. Подготовьте новый Pi по [`deploy/pi/README.md`](../pi/README.md), пункты 1–3: Docker,
|
||||||
|
`docker-compose.yml`, `.env`.
|
||||||
|
- Есть копия старого `.env` — просто положите её.
|
||||||
|
- Нет копии — заполните `.env` заново. **`BACKUP_PASSWORD` должен быть прежним.**
|
||||||
|
Если ключа `deploy/backup/id_backup` на ПК больше нет, сделайте новый (шаг 2), добавьте
|
||||||
|
его `.pub` на VPS (шаг 3, пункт 4) и укажите новый base64 в `BACKUP_SSH_KEY_B64`.
|
||||||
|
2. Запустите всё:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/forbidden-stars
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Дождитесь, пока приложение создаст пустую БД (`docker compose ps` → `app` `(healthy)`).
|
||||||
|
В журнале бэкапа это нормально (`docker compose logs backup`):
|
||||||
|
|
||||||
|
```
|
||||||
|
В БД нет ни игроков, ни партий, а последний снимок в репозитории vps — с данными.
|
||||||
|
Похоже на новый или очищенный сервер: бэкап НЕ сделан, чтобы пустые данные не вытеснили историю.
|
||||||
|
```
|
||||||
|
|
||||||
|
Это защита: пустой новый Pi не перезапишет историю на VPS.
|
||||||
|
|
||||||
|
4. Посмотрите снимки на VPS и выберите нужный (обычно самый свежий):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec backup fs-backup list vps
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Восстановите:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose stop app
|
||||||
|
docker compose exec backup fs-backup restore latest --repo vps --yes
|
||||||
|
docker compose start app
|
||||||
|
```
|
||||||
|
|
||||||
|
Вместо `latest` можно указать ID из `list vps`.
|
||||||
|
|
||||||
|
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`);
|
||||||
|
- раз в пару месяцев сделать учебное восстановление (`restore-test`);
|
||||||
|
- удалить с ПК старые архивы — они не зашифрованы.
|
||||||
|
- **Иногда:** посмотреть `docker compose ps`. Статус `unhealthy` у `backup` означает, что
|
||||||
|
бэкапы перестали проходить (причину покажет `fs-backup status`).
|
||||||
|
|
||||||
|
**Изменить расписание или сроки хранения.** Отредактируйте `BACKUP_*` в `.env` на Pi, затем
|
||||||
|
пересоздайте контейнер:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d backup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Удалить именованный или pre-restore снимок.** Автоматически они не удаляются, удалять
|
||||||
|
нужно в обоих репозиториях:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec backup fs-backup restic local forget <ID> --prune
|
||||||
|
docker compose exec backup fs-backup restic vps forget <ID-на-vps> --prune
|
||||||
|
```
|
||||||
|
|
||||||
|
ID одного и того же снимка в `local` и `vps` разные — смотрите `list` и `list vps`.
|
||||||
|
|
||||||
|
**Обновить образ бэкапа** (после изменений в `deploy/backup/`). На ПК —
|
||||||
|
`.\scripts\build-push.ps1`, на Pi — `docker compose up -d backup`.
|
||||||
|
|
||||||
|
> **Никогда не выполняйте на проде `docker compose down -v`.** Флаг `-v` удаляет тома —
|
||||||
|
> данные приложения **и** локальную копию бэкапов. Обычный `docker compose down` данные
|
||||||
|
> не трогает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Неполадки
|
||||||
|
|
||||||
|
Первое, что стоит сделать при любой проблеме:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/forbidden-stars
|
||||||
|
docker compose exec backup fs-backup status
|
||||||
|
docker compose logs --tail 100 backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### На Pi (журнал и команды `fs-backup`)
|
||||||
|
|
||||||
|
| Симптом | Причина | Что сделать |
|
||||||
|
|---|---|---|
|
||||||
|
| `BACKUP_PASSWORD не задан в .env — бэкапы ОТКЛЮЧЕНЫ` | нет пароля в `.env` | добавить `BACKUP_PASSWORD` (шаг 5), затем `docker compose up -d backup` |
|
||||||
|
| `неверный BACKUP_PASSWORD для репозитория …` | пароль в `.env` не тот, с которым создан репозиторий | вернуть правильный пароль из менеджера паролей; `docker compose up -d backup` |
|
||||||
|
| `BACKUP_SSH_KEY_B64 не декодируется из base64` / `— не приватный SSH-ключ` | строка ключа обрезана, с пробелами или от `.pub` | заново скопировать base64 **приватного** ключа (шаг 5, пункт 5), одной строкой |
|
||||||
|
| `Репозиторий vps недоступен` и выше `Permission denied (publickey)` | на VPS нет публичного ключа или ключ другой | шаг 3, пункты 4 и 9: проверить `authorized_keys` и вход `sftp` с ПК этим ключом |
|
||||||
|
| `Host key verification failed` | VPS переустановлен, у него новый ключ хоста | `docker compose exec backup rm /backup/state/known_hosts`, затем `docker compose exec backup fs-backup run` |
|
||||||
|
| `Репозиторий vps недоступен` и `Connection timed out` | VPS недоступен или неверный `BACKUP_VPS_HOST`/`PORT` | проверить VPS; локальная копия при этом продолжает делаться |
|
||||||
|
| `В БД нет ни игроков, ни партий, а последний снимок … — с данными` | новый или очищенный сервер — защита от затирания истории | новый Pi: [раздел 9](#9-катастрофа-pi-умер). Данные удалены намеренно: `fs-backup run --allow-empty` |
|
||||||
|
| `копия БД не прошла PRAGMA integrity_check` | живая БД повреждена | снимок не создаётся, старые целы. Восстановить последний хороший снимок ([раздел 8](#8-восстановление-прода)) |
|
||||||
|
| `приложение работает — восстанавливать поверх него нельзя` | не остановлен `app` | `docker compose stop app`, повторить команду |
|
||||||
|
| `найдены следы прерванного восстановления` | восстановление оборвалось (выключили питание и т.п.) | `docker compose stop app`, `docker compose exec backup fs-backup recover`, затем при необходимости повторить `restore` |
|
||||||
|
| `уже выполняется другая операция бэкапа` | идёт бэкап по расписанию или проверка | подождать: `docker compose logs -f backup` |
|
||||||
|
| `мало места в …` | диск Pi заполнен | `df -h`; удалить неиспользуемые образы (`docker image prune`); уменьшить `BACKUP_KEEP_*` |
|
||||||
|
| `снимок '…' не найден в репозитории` | опечатка в ID или снимок в другом репозитории | `fs-backup list` и `fs-backup list vps` |
|
||||||
|
| `service "backup" is not running` | контейнер не запущен | `docker compose up -d backup` |
|
||||||
|
| `permission denied while trying to connect to the Docker daemon socket` | пользователь Pi не в группе `docker` | `sudo usermod -aG docker $USER`, перезайти по SSH |
|
||||||
|
| `docker compose ps` → `backup` `unhealthy` | последний успешный бэкап старше `BACKUP_MAX_AGE_HOURS` | `docker compose exec backup fs-backup health` покажет причину, дальше по таблице |
|
||||||
|
|
||||||
|
**Про `recover`.** Команда сама определяет, на каком этапе оборвалось восстановление:
|
||||||
|
- если замена успела пройти во всех томах — дочищает промежуточные папки, восстановленные
|
||||||
|
данные остаются;
|
||||||
|
- иначе возвращает каждому тому прежние данные.
|
||||||
|
|
||||||
|
После неё можно спокойно запускать приложение или повторить восстановление.
|
||||||
|
|
||||||
|
**Пароль бэкапов потерян навсегда.** Прочитать существующие снимки невозможно. Начать заново
|
||||||
|
(**это удалит все старые бэкапы**):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose stop backup
|
||||||
|
docker compose rm -f backup
|
||||||
|
docker volume ls | grep backup-data # имя тома, обычно forbidden-stars_backup-data
|
||||||
|
docker volume rm <имя тома>
|
||||||
|
```
|
||||||
|
|
||||||
|
На VPS: `rm -rf /srv/fs-backups/restic`. Затем новый пароль в `.env`,
|
||||||
|
`docker compose up -d backup`.
|
||||||
|
|
||||||
|
### На ПК (скрипт `fs-backup.ps1`)
|
||||||
|
|
||||||
|
| Симптом | Причина | Что сделать |
|
||||||
|
|---|---|---|
|
||||||
|
| `running scripts is disabled on this system` | политика запуска PowerShell | `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` |
|
||||||
|
| `Set BACKUP_PI_SSH in .env` | не заполнен адрес Pi | шаг 6, пункт 3 |
|
||||||
|
| `ssh: connect to host … Connection timed out` | неверный IP Pi или ПК не в той сети | проверить `ssh pi@<IP-адрес-Pi>` вручную |
|
||||||
|
| Пароль Pi спрашивается несколько раз за команду | не настроен вход по ключу | шаг 6, пункт 2 |
|
||||||
|
| `WARNING: UNPROTECTED PRIVATE KEY FILE!` | у файла ключа слишком открытые права (ключ создан в Git Bash/WSL или скопирован) | `icacls <путь к ключу> /inheritance:r /grant:r "$($env:USERNAME):(R)"` |
|
||||||
|
| `Checksum mismatch … run pull again` | файл повредился при передаче | повторить `pull` (битый файл уже удалён) |
|
||||||
|
| `Already downloaded: …` | этот снимок уже скачан | ничего не делать; нужен новый — сначала `now`, потом `pull` |
|
||||||
|
| `restore-test`: `Import failed … The test clone data was not changed` | архив повреждён или неполный | скачать заново (`pull`); текст ошибки выше в выводе |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` | краткая справка |
|
||||||
|
|
||||||
|
Без `--yes` команды `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 |
|
||||||
|
| `restore-test -File <архив>` | учебное восстановление в локальный тест-клон |
|
||||||
|
| `-Target test` | выполнить `status`/`list`/`now`/`verify`/`pull` на локальном тест-клоне |
|
||||||
|
|
||||||
|
В bash-версии те же команды пишутся так: `list vps`, `now --tag имя`,
|
||||||
|
`pull <ID> --repo vps`, `restore-test <архив>`, `--test` первым аргументом.
|
||||||
|
|
||||||
|
### Переменные `.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)
|
||||||
|
- [ ] Учебное восстановление на тест-клоне прошло, данные на месте (шаг 7)
|
||||||
|
|
||||||
|
**Через сутки**
|
||||||
|
- [ ] В `list` появился снимок с меткой `scheduled` в 04:00
|
||||||
|
- [ ] В `list vps` — такой же
|
||||||
|
|
||||||
|
**Через неделю**
|
||||||
|
- [ ] В `status` есть строка `Последняя проверка данных: … данные целы`
|
||||||
+14
-4
@@ -111,10 +111,20 @@ docker compose logs -f tunnel # ждём строку: [tunnel] -R 9000:a
|
|||||||
Уже обеспечен: `systemctl enable docker` + `restart: unless-stopped`. После `sudo reboot`
|
Уже обеспечен: `systemctl enable docker` + `restart: unless-stopped`. После `sudo reboot`
|
||||||
контейнеры поднимутся сами (используют локальный образ, без повторного pull).
|
контейнеры поднимутся сами (используют локальный образ, без повторного pull).
|
||||||
|
|
||||||
## Бэкап (опционально)
|
## Бэкапы
|
||||||
Бэкап-скриптам нужен сам `docker-compose.yml` (он на Pi) + скрипты. Скопируй рядом
|
Отдельный контейнер `backup` в том же `docker-compose.yml`: каждую ночь делает зашифрованный
|
||||||
`scripts/backup.sh` и `scripts/restore.sh`, настрой `BACKUP_*` в `.env` и cron. Подробно —
|
снимок БД, `uploads` и `achievements` на Pi и на VPS. Дополнительных файлов на Pi не нужно,
|
||||||
комментарии в `scripts/backup.sh` и `deploy/vps/README.md` §8.
|
всё настраивается блоком `BACKUP_*` в `.env`. Пока `BACKUP_PASSWORD` пуст, бэкапы выключены.
|
||||||
|
|
||||||
|
Пошаговая настройка, восстановление и действия при гибели Pi —
|
||||||
|
[`deploy/backup/README.md`](../backup/README.md).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec backup fs-backup status # состояние
|
||||||
|
docker compose exec backup fs-backup list # хронология снимков
|
||||||
|
```
|
||||||
|
|
||||||
|
> Не выполняйте `docker compose down -v`: флаг `-v` удаляет тома с данными и локальными бэкапами.
|
||||||
|
|
||||||
## Если что-то не так
|
## Если что-то не так
|
||||||
- `https://forbiddenstars.ru` отдаёт заглушку/502 → туннель не поднят: `docker compose logs tunnel`
|
- `https://forbiddenstars.ru` отдаёт заглушку/502 → туннель не поднят: `docker compose logs tunnel`
|
||||||
|
|||||||
+12
-13
@@ -110,16 +110,15 @@ systemctl reload caddy
|
|||||||
Логи Caddy: `journalctl -u caddy -f`.
|
Логи Caddy: `journalctl -u caddy -f`.
|
||||||
|
|
||||||
## 8. Приём бэкапов с Pi (оффсайт-копии)
|
## 8. Приём бэкапов с Pi (оффсайт-копии)
|
||||||
Pi ежедневно шлёт полный архив (`scripts/backup.sh`) на VPS отдельным пользователем `backup`
|
Контейнер `backup` на Pi каждую ночь отправляет зашифрованный снимок (restic) на VPS по SFTP.
|
||||||
(не путать с `tunnel`). Каталог — **вне** веб-корней Caddy, права 700.
|
Для этого на VPS нужен отдельный пользователь `fsbackup` **только для SFTP**: без shell,
|
||||||
```bash
|
без туннелей, без пароля. Репозиторий лежит в `/srv/fs-backups/restic`, вне веб-корней
|
||||||
useradd -m -s /bin/bash backup
|
Caddy, так что бэкапы не публичны.
|
||||||
install -d -m 700 -o backup -g backup /home/backup/.ssh
|
|
||||||
install -m 600 -o backup -g backup /dev/null /home/backup/.ssh/authorized_keys
|
> Имя не `backup`: в Debian/Ubuntu системный пользователь `backup` уже существует.
|
||||||
install -d -m 700 -o backup -g backup /srv/fs-backups
|
|
||||||
# Публичный ключ бэкапа с Pi (deploy/backup/id_backup.pub) добавь в authorized_keys:
|
Пошаговая настройка: создание пользователя, ключ, `sshd_config.d/60-fs-backup.conf`,
|
||||||
# echo '<содержимое id_backup.pub>' >> /home/backup/.ssh/authorized_keys
|
проверки — [`deploy/backup/README.md`, шаг 3](../backup/README.md#шаг-3-vps-пользователь-только-для-sftp).
|
||||||
```
|
|
||||||
> Ротацию на VPS делает сам скрипт с Pi (`BACKUP_KEEP_REMOTE`, по умолч. 30). Каталог
|
Проверка приёма после первого бэкапа: `ls -la /srv/fs-backups/restic` (там `config`, `data`,
|
||||||
> `/srv/fs-backups` не отдаётся Caddy (нет `root`/`file_server` на него) → бэкапы не публичны.
|
`index`, `keys`, `snapshots`). Старые снимки удаляет сам контейнер с Pi по политике хранения.
|
||||||
> Проверка приёма: после `scripts/backup.sh` на Pi → `ls -1 /srv/fs-backups/` на VPS.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user