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