Compare commits
2 Commits
feaa31f702
...
f319133cee
| Author | SHA1 | Date | |
|---|---|---|---|
| f319133cee | |||
| 56cee83198 |
@@ -25,28 +25,32 @@
|
||||
│ └── ct105-vectors/ ← векторы RAG (vectors.npz) из CT 105
|
||||
├── restic/ ← (опционально)
|
||||
└── vps/
|
||||
└── miran/ ← VPS Миран: БД бота, voice_users, копия S3 (telegram-helper-bot)
|
||||
├── miran/ ← VPS Миран: БД бота, voice_users, копия S3 (telegram-helper-bot)
|
||||
└── mtproto-germany/ ← VPS Германия: конфиги MTProto + сайт katykhin.store (mtg, nginx, certs)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Что, откуда, куда, когда
|
||||
|
||||
| Что | Откуда | Куда (локально) | Когда | Хранение |
|
||||
|-----|--------|------------------|------|----------|
|
||||
| **LXC и VM** | Все выбранные контейнеры (100–109) и VM 200 | `/mnt/backup/proxmox/dump/` | **02:00** ежедневно (задание в Proxmox UI) | По настройкам задания (например: 7 daily, 4 weekly, 6 monthly) |
|
||||
| **Конфиги хоста** | `/etc/pve`, `/etc/network/interfaces`, `/etc/hosts`, `/etc/resolv.conf` | `/mnt/backup/proxmox/etc-pve/` | **02:15** ежедневно (cron: `backup-etc-pve.sh`) | 30 дней |
|
||||
| **БД Nextcloud (PostgreSQL)** | CT 101, контейнер `nextcloud-db-1` в `/opt/nextcloud` | `/mnt/backup/databases/ct101-nextcloud/` | **01:15** ежедневно (cron: `backup-ct101-pgdump.sh`) | 14 дней |
|
||||
| **БД Gitea (PostgreSQL)** | CT 103, контейнер `gitea-db-1` в `/opt/gitea` | `/mnt/backup/databases/ct103-gitea/` | **03:00** ежедневно (cron: `backup-ct103-gitea-pgdump.sh`) | 14 дней |
|
||||
| **БД Paperless (PostgreSQL)** | CT 104, контейнер `paperless-db-1` в `/opt/paperless` | `/mnt/backup/databases/ct104-paperless/` | **02:30** ежедневно (cron: `backup-ct104-pgdump.sh`) | 14 дней |
|
||||
| **Данные Vaultwarden (пароли)** | CT 103, `/opt/docker/vaultwarden/data` | `/mnt/backup/other/vaultwarden/` | **02:45** ежедневно (cron: `backup-vaultwarden-data.sh`); каталог в restic → Yandex | 14 дней |
|
||||
| **БД Immich (PostgreSQL)** | VM 200, контейнер `database` в `/opt/immich` | `/mnt/backup/databases/vm200-immich/` | **03:15** ежедневно (cron: `backup-vm200-pgdump.sh`) | 14 дней |
|
||||
| **Векторы RAG (CT 105)** | CT 105, `/home/rag-service/data/vectors/` (vectors.npz) | `/mnt/backup/other/ct105-vectors/` | **03:30** ежедневно (cron: `backup-ct105-vectors.sh`) | 14 дней |
|
||||
| **Оригиналы фото Immich** | VM 200, `/mnt/data/library/` | `/mnt/backup/photos/library/` | **01:30** ежедневно (cron: `backup-immich-photos.sh`, rsync) | Все копии (без автоудаления) |
|
||||
| **VPS Миран (telegram-helper-bot)** | VPS 185.147.80.190: БД `tg-bot-database.db`, каталог `voice_users`, бакет S3 (Miran) | `/mnt/backup/vps/miran/` (db/, voice_users/, s3/) | **01:00** ежедневно (cron: `backup-vps-miran.sh`) | БД: 14 дней; voice_users и S3 — перезапись |
|
||||
| **Выгрузка в Yandex (restic)** | `/mnt/backup` целиком | Yandex Object Storage | **04:00** ежедневно (cron: `backup-restic-yandex.sh`) | 3 daily, 2 weekly, 2 monthly |
|
||||
|
||||
**Окно бэкапов:** внутренние копии (синк внутри сервера) — **01:00–03:30**; выгрузка в облако — **04:00**. **05:00** зарезервировано под плановую перезагрузку сервера. Задание vzdump — из веб-интерфейса Proxmox (Центр обработки данных → Резервная копия).
|
||||
| Что | Откуда | Куда (локально) | Когда | Хранение | Уведомление |
|
||||
|-----|--------|------------------|------|----------|--------------|
|
||||
| **VPS Миран (telegram-helper-bot)** | VPS 185.147.80.190: БД `tg-bot-database.db`, каталог `voice_users`, бакет S3 (Miran) | `/mnt/backup/vps/miran/` (db/, voice_users/, s3/) | **01:00** (cron: `backup-vps-miran.sh`) | БД: 14 дней; voice_users и S3 — перезапись | 🖥️ VPS Миран |
|
||||
| **БД Nextcloud (PostgreSQL)** | CT 101, контейнер `nextcloud-db-1` в `/opt/nextcloud` | `/mnt/backup/databases/ct101-nextcloud/` | **01:15** (cron: `backup-ct101-pgdump.sh`) | 14 дней | 🗄️ Nextcloud (БД) |
|
||||
| **Оригиналы фото Immich** | VM 200, `/mnt/data/library/` | `/mnt/backup/photos/library/` | **01:30** (cron: `backup-immich-photos.sh`, rsync) | Все копии (без автоудаления) | 📷 Фото Immich (rsync) |
|
||||
| **Конфиги MTProto (VPS Германия)** | VPS 185.103.253.99: mtg.service, nginx (katykhin.store), Let's Encrypt, `/var/www/katykhin.store` | `/mnt/backup/vps/mtproto-germany/` (архивы mtproto-config-*.tar.gz) | **01:45** (cron: `backup-vps-mtproto.sh`) | 14 дней | 🌐 VPS MTProto (DE) |
|
||||
| **LXC и VM** | Все выбранные контейнеры (100–109) и VM 200 | `/mnt/backup/proxmox/dump/` | **02:00** (задание в Proxmox UI) | По настройкам задания (7 daily, 4 weekly, 6 monthly) | 💾 Backup local (cron 03:00) |
|
||||
| **Конфиги хоста** | `/etc/pve`, `/etc/network/interfaces`, `/etc/hosts`, `/etc/resolv.conf` | `/mnt/backup/proxmox/etc-pve/` | **02:15** (cron: `backup-etc-pve.sh`) | 30 дней | ⚙️ Конфиги хоста |
|
||||
| **БД Paperless (PostgreSQL)** | CT 104, контейнер `paperless-db-1` в `/opt/paperless` | `/mnt/backup/databases/ct104-paperless/` | **02:30** (cron: `backup-ct104-pgdump.sh`) | 14 дней | 🗄️ Paperless (БД) |
|
||||
| **Данные Vaultwarden (пароли)** | CT 103, `/opt/docker/vaultwarden/data` | `/mnt/backup/other/vaultwarden/`; каталог в restic → Yandex | **02:45** (cron: `backup-vaultwarden-data.sh`) | 14 дней | 🔐 Vaultwarden |
|
||||
| **БД Gitea (PostgreSQL)** | CT 103, контейнер `gitea-db-1` в `/opt/gitea` | `/mnt/backup/databases/ct103-gitea/` | **03:00** (cron: `backup-ct103-gitea-pgdump.sh`) | 14 дней | 🗄️ Gitea (БД) |
|
||||
| **БД Immich (PostgreSQL)** | VM 200, контейнер `database` в `/opt/immich` | `/mnt/backup/databases/vm200-immich/` | **03:15** (cron: `backup-vm200-pgdump.sh`) | 14 дней | 🗄️ Immich (БД) |
|
||||
| **Векторы RAG (CT 105)** | CT 105, `/home/rag-service/data/vectors/` (vectors.npz) | `/mnt/backup/other/ct105-vectors/` | **03:30** (cron: `backup-ct105-vectors.sh`) | 14 дней | 📐 Векторы RAG |
|
||||
| **Выгрузка в Yandex (restic)** | `/mnt/backup` без photos | Yandex Object Storage | **04:00** (cron: `backup-restic-yandex.sh`) | 3 daily, 2 weekly, 2 monthly | ☁️ Restic Yandex |
|
||||
| **Выгрузка в Yandex (restic, фото)** | `/mnt/backup/photos` | Yandex Object Storage (тот же репо) | **04:10** (cron: `backup-restic-yandex-photos.sh`) | 3 daily, 2 weekly, 2 monthly | 📷 Restic Yandex (photos) |
|
||||
|
||||
**Окно бэкапов:** внутренние копии — **01:00–03:30**; выгрузка в облако — **04:00** (основной restic), **04:10** (restic фото). **05:00** зарезервировано под плановую перезагрузку сервера. Задание vzdump — из веб-интерфейса Proxmox (Центр обработки данных → Резервная копия).
|
||||
|
||||
---
|
||||
|
||||
@@ -56,36 +60,56 @@
|
||||
|
||||
**Когда нужно:** потеря или поломка одной/нескольких гостевых систем.
|
||||
|
||||
1. В Proxmox: **Центр обработки данных → Резервная копия** (или узел → Резервная копия).
|
||||
**Через веб-интерфейс:**
|
||||
|
||||
1. **Центр обработки данных → Резервная копия** (или узел → Резервная копия).
|
||||
2. Выбрать хранилище **backup** (или то, куда пишет задание).
|
||||
3. Найти нужный бэкап по VMID и дате.
|
||||
4. **Восстановить** → указать новый VMID (если восстанавливаем как копию) или тот же (если заменяем сломанный), узел и storage для дисков.
|
||||
5. Запустить ВМ/контейнер и проверить доступность.
|
||||
|
||||
**С CLI (на хосте):**
|
||||
**С CLI (на хосте Proxmox):**
|
||||
|
||||
- LXC: `pct restore <vmid> /path/to/backup.vma.zst --storage local-lvm` (и т.п., см. `pct restore --help`).
|
||||
- VM: `qm restore <vmid> /path/to/backup.vma.zst` (и т.п., см. `qm restore --help`).
|
||||
Путь к файлам бэкапа: `/mnt/backup/proxmox/dump/dump/` (имя вида `vzdump-lxc-100-YYYY_MM_DD-HH_MM_SS.tar.zst` или `vzdump-qemu-200-...`).
|
||||
|
||||
Путь к файлу бэкапа на хосте: `/mnt/backup/proxmox/dump/` (имя файла вида `vzdump-lxc-100-...` или `vzdump-qemu-200-...`).
|
||||
- **LXC** — восстановить в новый VMID (например 999) на storage `local-lvm`:
|
||||
```bash
|
||||
pct create 999 /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-2026_02_26-02_03_14.tar.zst --restore 1 --storage local-lvm
|
||||
```
|
||||
Если восстанавливаем поверх существующего контейнера: сначала удалить его (`pct destroy 107`), затем в команде указать тот же VMID (107). Доп. опции: `pct create --help` (режим restore).
|
||||
- **VM (KVM)** — порядок аргументов: сначала архив, потом VMID:
|
||||
```bash
|
||||
qm restore /mnt/backup/proxmox/dump/dump/vzdump-qemu-200-YYYY_MM_DD-HH_MM_SS.vma.zst 200 --storage local-lvm
|
||||
```
|
||||
У VM файлы бэкапа обычно с расширением `.vma.zst` или `.vma`. Подробнее: `qm restore --help`.
|
||||
|
||||
**После восстановления (пример для LXC):**
|
||||
|
||||
- Если восстановили в новый слот (например 999) и не нужен конфликт IP с оригиналом — сменить IP:
|
||||
`pct set 999 --net0 name=eth0,bridge=vmbr0,gw=192.168.1.1,ip=192.168.1.199/24,type=veth`
|
||||
- Запуск: `pct start 999` (LXC) или `qm start 200` (VM).
|
||||
- Проверка: пинг, консоль (`pct exec 999 -- bash`), при необходимости сервисы и порты внутри контейнера.
|
||||
|
||||
Если vzdump есть только в Yandex (локального диска нет) — см. раздел **Восстановление из restic (Yandex)** → «Восстановление одного контейнера (vzdump)».
|
||||
|
||||
---
|
||||
|
||||
### 2. Восстановление конфигов хоста (/etc/pve и сеть)
|
||||
|
||||
**Когда нужно:** переустановка Proxmox или потеря конфигов узла (при этом диск с бэкапами доступен).
|
||||
**Когда нужно:** переустановка Proxmox или потеря конфигов узла (при этом диск с бэкапами доступен).
|
||||
Если конфигов нет локально, но есть в Yandex — см. раздел **Восстановление из restic** → «Восстановление конфигов хоста (/etc/pve)».
|
||||
|
||||
1. Скопировать нужный архив с хоста, например:
|
||||
`etc-pve-YYYYMMDD-HHMM.tar.gz` и/или `etc-host-configs-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/proxmox/etc-pve/`.
|
||||
1. Скопировать нужный архив с хоста, например:
|
||||
`etc-pve-YYYYMMDD-HHMM.tar.gz` и/или `etc-host-configs-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/proxmox/etc-pve/`.
|
||||
2. **Восстановление /etc/pve** (на переустановленном хосте, от root):
|
||||
```bash
|
||||
```bash
|
||||
tar -xzf etc-pve-YYYYMMDD-HHMM.tar.gz -C /
|
||||
```
|
||||
```
|
||||
При одномузловой установке обычно достаточно распаковать в `/`. При кластере — аккуратно с нодами и storage.
|
||||
3. **Восстановление конфигов сети/хоста** (interfaces, hosts, resolv.conf):
|
||||
```bash
|
||||
```bash
|
||||
tar -xzf etc-host-configs-YYYYMMDD-HHMM.tar.gz -C /
|
||||
```
|
||||
```
|
||||
При необходимости поправить под текущее железо (интерфейсы, IP) и перезапустить сеть.
|
||||
|
||||
После восстановления конфигов — заново добавить storage для бэкапов (если переустанавливали с нуля) и восстанавливать гостей из vzdump по шагу 1.
|
||||
@@ -96,17 +120,14 @@
|
||||
|
||||
**Когда нужно:** повреждение или потеря базы Immich при рабочей ВМ (образ VM можно не трогать, восстанавливаем только БД).
|
||||
|
||||
1. Скопировать нужный дамп на VM 200, например:
|
||||
`immich-db-YYYYMMDD-HHMM.sql.gz` из `/mnt/backup/databases/vm200-immich/`.
|
||||
2. На VM 200 (ssh admin@192.168.1.200):
|
||||
```bash
|
||||
1. Скопировать нужный дамп на VM 200, например:
|
||||
`immich-db-YYYYMMDD-HHMM.sql.gz` из `/mnt/backup/databases/vm200-immich/`.
|
||||
2. На VM 200 (ssh [admin@192.168.1.200](mailto:admin@192.168.1.200)):
|
||||
```bash
|
||||
cd /opt/immich
|
||||
gunzip -c /path/to/immich-db-YYYYMMDD-HHMM.sql.gz | docker compose exec -T database psql -U <DB_USERNAME> -d <DB_DATABASE_NAME>
|
||||
```
|
||||
```
|
||||
Или распаковать `.sql.gz`, затем:
|
||||
```bash
|
||||
docker compose exec -T database psql -U <DB_USERNAME> -d <DB_DATABASE_NAME> < backup.sql
|
||||
```
|
||||
`<DB_USERNAME>` и `<DB_DATABASE_NAME>` — из `/opt/immich/.env` (обычно `postgres` и `immich`).
|
||||
|
||||
Перед восстановлением лучше остановить приложение Immich (или как минимум не писать в БД). При полной пересоздании БД — очистить каталог данных PostgreSQL в контейнере и затем загрузить дамп.
|
||||
@@ -117,6 +138,8 @@
|
||||
|
||||
**Когда нужно:** потеря данных на диске VM 200 (например `/mnt/data/library`).
|
||||
|
||||
**Требование для бэкапа:** на VM 200 должен быть установлен rsync (`sudo apt install rsync`), т.к. скрипт запускает rsync по SSH с хоста.
|
||||
|
||||
На VM 200 (или с хоста через rsync в обратную сторону): скопировать содержимое `/mnt/backup/photos/library/` обратно в каталог библиотеки Immich на VM 200 (в .env указан `UPLOAD_LOCATION`, обычно `/mnt/data/library`). Пример с хоста Proxmox:
|
||||
|
||||
```bash
|
||||
@@ -125,6 +148,8 @@ rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/
|
||||
|
||||
После копирования на VM 200 выставить владельца/права под контейнер Immich (если нужно) и перезапустить сервисы.
|
||||
|
||||
Если фото есть только в Yandex — см. раздел **Восстановление из restic** → «Восстановление фото (библиотека Immich)».
|
||||
|
||||
---
|
||||
|
||||
### 5. Восстановление БД Paperless (CT 104), Gitea (CT 103)
|
||||
@@ -133,12 +158,16 @@ rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/
|
||||
|
||||
**Gitea:** дамп из `/mnt/backup/databases/ct103-gitea/gitea-db-*.sql.gz`. На CT 103: остановить Gitea, восстановить в контейнер `gitea-db-1` (psql -U gitea -d gitea), запустить стек.
|
||||
|
||||
Если дампов нет локально — см. раздел **Восстановление из restic** → «Восстановление дампов БД».
|
||||
|
||||
---
|
||||
|
||||
### 6. Восстановление данных Vaultwarden (CT 103)
|
||||
|
||||
Архив из `/mnt/backup/other/vaultwarden/vaultwarden-data-*.tar.gz`. На CT 103: остановить Vaultwarden, распаковать в `/opt/docker/vaultwarden/` (получится каталог `data/`), выставить владельца/права под контейнер, запустить Vaultwarden.
|
||||
|
||||
Если архива нет локально (есть только в Yandex) — см. раздел **Восстановление из restic** → «Восстановление данных Vaultwarden (пароли)».
|
||||
|
||||
---
|
||||
|
||||
### 7. Восстановление бэкапа VPS Миран (telegram-helper-bot)
|
||||
@@ -146,40 +175,68 @@ rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/
|
||||
**Когда нужно:** потеря данных на VPS или перенос бота на другой хост.
|
||||
|
||||
В бэкапе есть:
|
||||
|
||||
- **БД:** `/mnt/backup/vps/miran/db/tg-bot-database-YYYYMMDD.db` — копии SQLite.
|
||||
- **Голосовые сообщения:** `/mnt/backup/vps/miran/voice_users/` — каталог .ogg.
|
||||
- **S3 (контент бота):** `/mnt/backup/vps/miran/s3/` — полная копия бакета (photos, videos, voice и т.д.).
|
||||
|
||||
**Восстановление на VPS:**
|
||||
1. Скопировать выбранный файл БД на VPS:
|
||||
`scp -P 15722 /mnt/backup/vps/miran/db/tg-bot-database-YYYYMMDD.db deploy@185.147.80.190:/home/prod/bots/telegram-helper-bot/database/tg-bot-database.db`
|
||||
2. Восстановить `voice_users`:
|
||||
`rsync -avz -e "ssh -p 15722" /mnt/backup/vps/miran/voice_users/ deploy@185.147.80.190:/home/prod/bots/telegram-helper-bot/voice_users/`
|
||||
|
||||
1. Скопировать выбранный файл БД на VPS:
|
||||
`scp -P 15722 /mnt/backup/vps/miran/db/tg-bot-database-YYYYMMDD.db deploy@185.147.80.190:/home/prod/bots/telegram-helper-bot/database/tg-bot-database.db`
|
||||
2. Восстановить `voice_users`:
|
||||
`rsync -avz -e "ssh -p 15722" /mnt/backup/vps/miran/voice_users/ deploy@185.147.80.190:/home/prod/bots/telegram-helper-bot/voice_users/`
|
||||
3. При потере данных в S3 — загрузить из бэкапа в бакет Miran (через aws s3 sync или панель), используя endpoint `https://api.s3.miran.ru` и креды из [VPS Миран](vps-miran-bots.md).
|
||||
|
||||
**Требования для бэкапа:** на хосте Proxmox — SSH-ключ root → deploy@185.147.80.190 (порт 15722); для S3 — установленный `aws` cli и файл `/root/.vps-miran-s3.env` с переменными S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME (см. [VPS Миран](../vps/vps-miran-bots.md)).
|
||||
**Требования для бэкапа:** на хосте Proxmox — SSH-ключ root → [deploy@185.147.80.190](mailto:deploy@185.147.80.190) (порт 15722); для S3 — установленный `aws` cli и файл `/root/.vps-miran-s3.env` с переменными S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME (см. [VPS Миран](../vps/vps-miran-bots.md)).
|
||||
|
||||
---
|
||||
|
||||
### 8. Восстановление векторов RAG (CT 105)
|
||||
### 8. Восстановление конфигов MTProto (VPS Германия)
|
||||
|
||||
**Когда нужно:** потеря конфигов на VPS 185.103.253.99 или перенос MTProto и сайта-заглушки на другой хост.
|
||||
|
||||
В архиве `mtproto-config-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/vps/mtproto-germany/` лежат:
|
||||
|
||||
- **mtg:** `etc/systemd/system/mtg.service` (в т.ч. секрет и cloak-port).
|
||||
- **nginx:** `etc/nginx/sites-available/`, `etc/nginx/sites-enabled/` (конфиг для katykhin.store на порту 993).
|
||||
- **Let's Encrypt:** `etc/letsencrypt/live/katykhin.store/`, `archive/katykhin.store/`, `renewal/katykhin.store.conf`.
|
||||
- **Сайт:** `var/www/katykhin.store/`.
|
||||
|
||||
**Восстановление на VPS (от root):** скопировать архив на сервер и распаковать в корень:
|
||||
|
||||
```bash
|
||||
scp /mnt/backup/vps/mtproto-germany/mtproto-config-YYYYMMDD-HHMM.tar.gz root@185.103.253.99:/tmp/
|
||||
ssh root@185.103.253.99 "tar -xzf /tmp/mtproto-config-YYYYMMDD-HHMM.tar.gz -C /"
|
||||
```
|
||||
|
||||
После распаковки: `systemctl daemon-reload && systemctl restart mtg nginx`. На новом хосте дополнительно установить mtg, nginx, certbot и настроить ufw (см. [план MTProto + сайт](../vps/vpn-vps-mtproto-site-plan.md)).
|
||||
|
||||
**Требования для бэкапа:** на хосте Proxmox — SSH по ключу root → [root@185.103.253.99](mailto:root@185.103.253.99) (порт 22). Ключ хоста должен быть добавлен в `authorized_keys` на VPS.
|
||||
|
||||
---
|
||||
|
||||
### 9. Восстановление векторов RAG (CT 105)
|
||||
|
||||
Архив из `/mnt/backup/other/ct105-vectors/vectors-*.tar.gz`. Распаковать на хосте и скопировать в контейнер: `tar -xzf vectors-*.tar.gz` → затем `pct push 105 ./vectors /home/rag-service/data/` или распаковать внутри CT 105 в `/home/rag-service/data/` (получится каталог `vectors/` с `vectors.npz`).
|
||||
|
||||
---
|
||||
|
||||
### 9. Восстановление VM 200 (Immich) с нуля
|
||||
### 10. Восстановление VM 200 (Immich) с нуля
|
||||
|
||||
VM 200 **не входит** в задание vzdump (образ ~380 ГБ, не помещается в политику 7 копий). В бэкапе есть: **конфиг ВМ** (в архивах `/etc/pve`), **БД** (pg_dump), **фото** (rsync в `photos/library`). Восстановление — создание новой ВМ с теми же параметрами и перенос данных.
|
||||
|
||||
**Что есть после восстановления хоста:**
|
||||
|
||||
- Из бэкапа `etc-pve`: файл `/etc/pve/qemu-server/200.conf` — полное описание ВМ (CPU, память, диски, **hostpci для GPU**, сеть). Его можно использовать как образец при создании новой ВМ.
|
||||
- Дамп БД: `/mnt/backup/databases/vm200-immich/immich-db-*.sql.gz`.
|
||||
- Фото: `/mnt/backup/photos/library/`.
|
||||
|
||||
**Ключевые параметры VM 200** (если восстанавливать вручную без конфига):
|
||||
|
||||
- **Ресурсы:** 3 ядра, 10 GB RAM.
|
||||
- **GPU:** проброс видеокарты (hostpci) — в Proxmox: Hardware → Add → PCI Device → выбрать VGA/NVIDIA, поставить «All Functions» и «ROM-Bar» при необходимости. В конфиге это выглядит как `hostpci0: 0000:xx:00.0` и т.п.
|
||||
- **Диски:** первый — системный (~35 GB), второй — данные (~350 GB) под `/mnt/data` (библиотека, PostgreSQL, Docker).
|
||||
- **Диски:** первый — системный (~~35 GB), второй — данные (~~350 GB) под `/mnt/data` (библиотека, PostgreSQL, Docker).
|
||||
- **Сеть:** статический IP 192.168.1.200/24, шлюз 192.168.1.1.
|
||||
- **ОС:** Debian 13 (trixie), пользователь **admin**, SSH.
|
||||
|
||||
@@ -190,8 +247,8 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н
|
||||
3. **Разметить второй диск** и смонтировать в `/mnt/data` (как в [container-200](../containers/container-200.md)).
|
||||
4. **Установить Docker**, склонировать/восстановить каталоги Immich: `/opt/immich/` (docker-compose.yml, .env — из своих заметок или копии; секреты из Vaultwarden).
|
||||
5. **Создать каталоги** `/mnt/data/library`, `/mnt/data/postgres` (и др. по .env).
|
||||
6. **Скопировать фото** с хоста бэкапов на ВМ:
|
||||
`rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/`
|
||||
6. **Скопировать фото** с хоста бэкапов на ВМ:
|
||||
`rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/`
|
||||
7. **Запустить только контейнер БД** (database), восстановить дамп (см. раздел 3 выше), затем поднять весь стек Immich.
|
||||
8. Проверить NPM (прокси на 192.168.1.200:2283), при необходимости заново включить ML и настройки в Immich.
|
||||
|
||||
@@ -201,27 +258,266 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н
|
||||
|
||||
## Restic и Yandex
|
||||
|
||||
Скрипт **`backup-restic-yandex.sh`** выгружает весь каталог `/mnt/backup` в Yandex Object Storage (S3). Retention: **3 daily, 2 weekly, 2 monthly** (`restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2`). Пароли и дампы — чувствительные данные; не выкладывать в открытый доступ.
|
||||
Два задания в одном репозитории: `**backup-restic-yandex.sh`** выгружает `/mnt/backup` **без** каталога `photos`; `**backup-restic-yandex-photos.sh`** выгружает только `/mnt/backup/photos` (отдельный снимок, больше всего данных). Retention у обоих: **3 daily, 2 weekly, 2 monthly**. Пароли и дампы — чувствительные данные; не выкладывать в открытый доступ.
|
||||
|
||||
---
|
||||
|
||||
## Восстановление из restic (Yandex)
|
||||
|
||||
**Когда нужно:** локальных бэкапов нет (потеря диска, другой хост), данные есть только в Yandex Object Storage.
|
||||
|
||||
### Подготовка на хосте
|
||||
|
||||
- Те же креды, что для бэкапа: `**/root/.restic-yandex.env`** (RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), `**/root/.restic-password**`.
|
||||
- Установлены **restic** и **FUSE** (для `restic mount`): `apt install restic fuse`.
|
||||
- Восстановление делаем **на раздел с достаточным местом** (например `/mnt/backup/restore-...`), не в `/tmp`.
|
||||
|
||||
### Два типа снимков в одном репо
|
||||
|
||||
В репозитории два вида снимков (различаются по полю **Paths** в `restic snapshots`):
|
||||
|
||||
|
||||
| Paths в снимке | Откуда | Что внутри |
|
||||
| -------------------- | ------------------------------ | ------------------------------------------------------------------------- |
|
||||
| `/mnt/backup` | backup-restic-yandex.sh | Всё кроме photos: proxmox/dump, proxmox/etc-pve, databases/, other/, vps/ |
|
||||
| `/mnt/backup/photos` | backup-restic-yandex-photos.sh | Только каталог photos (библиотека Immich) |
|
||||
|
||||
|
||||
При восстановлении **конфигов, паролей, дампов БД, vzdump** — брать снимок с path **/mnt/backup**. При восстановлении **фото** — брать снимок с path **/mnt/backup/photos**.
|
||||
|
||||
```bash
|
||||
# Список снимков (указать нужный по Paths и дате)
|
||||
set -a; source /root/.restic-yandex.env; set +a
|
||||
export RESTIC_PASSWORD_FILE=/root/.restic-password
|
||||
export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-ru-central1}
|
||||
restic snapshots
|
||||
```
|
||||
|
||||
### Сводка: что откуда восстанавливать
|
||||
|
||||
|
||||
| Что восстановить | Путь в снимке (основной репо) | Снимок | Способ |
|
||||
| -------------------- | --------------------------------- | ---------------------- | ------------------------------- |
|
||||
| Один LXC/VM (vzdump) | /mnt/backup/proxmox/dump/dump/... | /mnt/backup | Скрипт mount + cp (см. ниже) |
|
||||
| Конфиги /etc/pve | /mnt/backup/proxmox/etc-pve/ | /mnt/backup | restic restore --path ... |
|
||||
| Vaultwarden (пароли) | /mnt/backup/other/vaultwarden/ | /mnt/backup | restic restore --path ... |
|
||||
| Дампы БД | /mnt/backup/databases/... | /mnt/backup | restic restore --path ... |
|
||||
| VPS, other | /mnt/backup/vps/, other/ | /mnt/backup | restic restore --path ... |
|
||||
| Фото Immich | /mnt/backup/photos/ | **/mnt/backup/photos** | restic restore из снимка photos |
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Восстановление одного контейнера (vzdump) из restic
|
||||
|
||||
Чтобы не выкачивать весь репо, используется **mount** и копирование одного файла.
|
||||
|
||||
1. Узнать имя нужного архива в снимке (например CT 107):
|
||||
```bash
|
||||
restic ls latest --path /mnt/backup/proxmox/dump/dump | grep vzdump-lxc-107
|
||||
```
|
||||
Использовать снимок с path `/mnt/backup` (не photos).
|
||||
2. Запустить скрипт (он сам монтирует, копирует файл, размонтирует):
|
||||
```bash
|
||||
/root/scripts/restore-one-vzdump-from-restic.sh latest /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-YYYY_MM_DD-HH_MM_SS.tar.zst /mnt/backup
|
||||
```
|
||||
Файл появится в `/mnt/backup/vzdump-lxc-107-....tar.zst`.
|
||||
3. Восстановить контейнер из файла (как в разделе 1 выше):
|
||||
```bash
|
||||
pct create 999 /mnt/backup/vzdump-lxc-107-....tar.zst --restore 1 --storage local-lvm
|
||||
pct set 999 --net0 name=eth0,bridge=vmbr0,gw=192.168.1.1,ip=192.168.1.199/24,type=veth
|
||||
pct start 999
|
||||
```
|
||||
|
||||
Если скрипта нет — вручную: `restic mount /mnt/backup/restic-mount &`, подождать, скопировать из `.../restic-mount/ids/<SNAPSHOT_ID>/mnt/backup/proxmox/dump/dump/vzdump-lxc-107-....tar.zst` в нужное место, затем `fusermount -u /mnt/backup/restic-mount`.
|
||||
|
||||
---
|
||||
|
||||
### Восстановление конфигов хоста (/etc/pve) из restic
|
||||
|
||||
1. Выбрать снимок с path **/mnt/backup** (по дате): `restic snapshots`.
|
||||
2. Восстановить только каталог etc-pve:
|
||||
```bash
|
||||
restic restore SNAPSHOT_ID --target /mnt/backup/restore-etc-pve --path /mnt/backup/proxmox/etc-pve
|
||||
```
|
||||
Файлы появятся в `/mnt/backup/restore-etc-pve/mnt/backup/proxmox/etc-pve/` (архивы `etc-pve-*.tar.gz`, `etc-host-configs-*.tar.gz`).
|
||||
3. Распаковать нужный архив в корень (как в разделе 2 выше):
|
||||
```bash
|
||||
tar -xzf /mnt/backup/restore-etc-pve/mnt/backup/proxmox/etc-pve/etc-pve-YYYYMMDD-HHMM.tar.gz -C /
|
||||
tar -xzf /mnt/backup/restore-etc-pve/mnt/backup/proxmox/etc-pve/etc-host-configs-YYYYMMDD-HHMM.tar.gz -C /
|
||||
```
|
||||
4. При необходимости поправить сеть и перезапустить сервисы.
|
||||
|
||||
---
|
||||
|
||||
### Восстановление данных Vaultwarden (пароли) из restic
|
||||
|
||||
1. Снимок с path **/mnt/backup**.
|
||||
2. Восстановить каталог vaultwarden:
|
||||
```bash
|
||||
restic restore SNAPSHOT_ID --target /mnt/backup/restore-vw --path /mnt/backup/other/vaultwarden
|
||||
```
|
||||
Результат: `/mnt/backup/restore-vw/mnt/backup/other/vaultwarden/vaultwarden-data-*.tar.gz`.
|
||||
3. Скопировать архив на хост, откуда можно отправить на CT 103, либо распаковать во временный каталог и скопировать каталог `data/` на CT 103 в `/opt/docker/vaultwarden/` (остановив Vaultwarden). Детали — раздел 6 выше.
|
||||
|
||||
---
|
||||
|
||||
### Восстановление фото (библиотека Immich) из restic
|
||||
|
||||
Фото лежат в **отдельном снимке** (path `/mnt/backup/photos`). Сначала выбрать этот снимок:
|
||||
|
||||
```bash
|
||||
restic snapshots | grep photos
|
||||
```
|
||||
|
||||
Затем восстановить в каталог с достаточным местом:
|
||||
|
||||
```bash
|
||||
restic restore SNAPSHOT_ID --target /mnt/backup/restore-photos --path /mnt/backup/photos
|
||||
```
|
||||
|
||||
Фото окажутся в `/mnt/backup/restore-photos/mnt/backup/photos/library/`. Дальше — скопировать на VM 200 (раздел 4 выше):
|
||||
`rsync -av /mnt/backup/restore-photos/mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/`
|
||||
|
||||
---
|
||||
|
||||
### Восстановление дампов БД из restic
|
||||
|
||||
Снимок с path **/mnt/backup**. Восстановить нужный подкаталог, например только ct104-paperless:
|
||||
|
||||
```bash
|
||||
restic restore SNAPSHOT_ID --target /mnt/backup/restore-db --path /mnt/backup/databases/ct104-paperless
|
||||
```
|
||||
|
||||
Файлы появятся в `/mnt/backup/restore-db/mnt/backup/databases/ct104-paperless/`. Дальше — восстановление БД по разделам 3 или 5 выше (скопировать дамп на контейнер и загрузить в PostgreSQL).
|
||||
|
||||
Аналогично для других БД: `--path /mnt/backup/databases/ct101-nextcloud`, `ct103-gitea`, `vm200-immich`.
|
||||
|
||||
---
|
||||
|
||||
### Восстановление прочего (VPS, векторы RAG) из restic
|
||||
|
||||
- **VPS Миран / MTProto:**
|
||||
`restic restore SNAPSHOT_ID --target /mnt/backup/restore-vps --path /mnt/backup/vps`
|
||||
Дальше — копировать нужные файлы на VPS по разделам 7–8.
|
||||
- **Векторы RAG (ct105-vectors):**
|
||||
`restic restore SNAPSHOT_ID --target /mnt/backup/restore-other --path /mnt/backup/other/ct105-vectors`
|
||||
Дальше — по разделу 9.
|
||||
|
||||
---
|
||||
|
||||
## Скрипты на хосте Proxmox
|
||||
|
||||
|
||||
| Скрипт | Назначение | Cron |
|
||||
|--------|------------|------|
|
||||
| `/root/scripts/backup-vps-miran.sh` | Бэкап VPS Миран: БД бота, voice_users, S3 (Miran) | 0 1 * * * |
|
||||
| `/root/scripts/backup-ct101-pgdump.sh` | Логический дамп БД Nextcloud из CT 101 | 15 1 * * * |
|
||||
| `/root/scripts/backup-immich-photos.sh` | Копирование библиотеки фото Immich (rsync с VM 200) | 30 1 * * * |
|
||||
| `/root/scripts/backup-vps-mtproto.sh` | Копирование конфигов MTProto + сайт с VPS Германия (185.103.253.99) | 45 1 * * * |
|
||||
| `/root/scripts/backup-etc-pve.sh` | Бэкап /etc/pve и конфигов хоста | 15 2 * * * |
|
||||
| `/root/scripts/backup-ct104-pgdump.sh` | Логический дамп БД Paperless из CT 104 | 30 2 * * * |
|
||||
| `/root/scripts/backup-vaultwarden-data.sh` | Копирование данных Vaultwarden (пароли) из CT 103 | 45 2 * * * |
|
||||
| `/root/scripts/backup-ct103-gitea-pgdump.sh` | Логический дамп БД Gitea из CT 103 | 0 3 * * * |
|
||||
| `/root/scripts/notify-vzdump-success.sh` | Проверка локального vzdump за последние 2 ч, отправка сводки в Telegram | 0 3 * * * |
|
||||
| `/root/scripts/backup-vm200-pgdump.sh` | Логический дамп БД Immich с VM 200 | 15 3 * * * |
|
||||
| `/root/scripts/backup-ct105-vectors.sh` | Копирование векторов RAG (vectors.npz) из CT 105 | 30 3 * * * |
|
||||
| `/root/scripts/backup-restic-yandex.sh` | Выгрузка /mnt/backup в Yandex S3 (restic), retention 3/2/2 | 0 4 * * * |
|
||||
| `/root/scripts/backup-restic-yandex.sh` | Выгрузка /mnt/backup (без photos) в Yandex S3 (restic), retention 3/2/2 | 0 4 * * * |
|
||||
| `/root/scripts/backup-restic-yandex-photos.sh` | Выгрузка только /mnt/backup/photos в Yandex S3 (тот же репо), retention 3/2/2 | 10 4 * * * |
|
||||
| `/root/scripts/notify-telegram.sh` | Шлюз отправки уведомлений в Telegram (вызывают скрипты бэкапов) | — |
|
||||
|
||||
|
||||
Задание vzdump (LXC/VM) настраивается в Proxmox UI (расписание 02:00). **05:00** оставлено свободным для плановой перезагрузки сервера.
|
||||
|
||||
### Диагностика пустых дампов БД и архива Vaultwarden
|
||||
|
||||
Если дампы БД (Nextcloud, Paperless, Gitea), архив Vaultwarden или векторы RAG (CT 105) получаются по 20 байт — в копию попал только пустой gzip/tar, команда внутри контейнера не отдала данные. Скрипты при размере < 512 байт завершаются с ошибкой и выводят stderr. Для векторов проверьте путь `/home/rag-service/data/vectors` в CT 105: `pct exec 105 -- ls -la /home/rag-service/data/vectors`.
|
||||
|
||||
**Частая причина дампов БД:** PostgreSQL в контейнере требует пароль (md5/scram). Скрипты берут пароль из **Vaultwarden** (Bitwarden CLI `bw`): объекты **NEXTCLOUD** (поле `dbpassword` или пароль), **PAPERLESS**, **GITEA**. На хосте нужны: `bw`, при необходимости `jq`, разблокировка по мастер-паролю из файла `/root/.bw-master` (см. [Переключение скриптов на секреты из Vaultwarden](proxmox-phase1-backup.md#переключение-скриптов-на-секреты-из-vaultwarden) в proxmox-phase1-backup.md).
|
||||
|
||||
**Проверка вручную (без подавления stderr):** зайти в контейнер и выполнить дамп, чтобы увидеть сообщение об ошибке:
|
||||
|
||||
```bash
|
||||
# CT 101 (Nextcloud)
|
||||
pct exec 101 -- docker exec nextcloud-db-1 pg_dump -U nextcloud nextcloud | head -5
|
||||
|
||||
# CT 104 (Paperless)
|
||||
pct exec 104 -- docker exec paperless-db-1 pg_dump -U paperless paperless | head -5
|
||||
|
||||
# CT 103 (Gitea)
|
||||
pct exec 103 -- docker exec gitea-db-1 pg_dump -U gitea gitea | head -5
|
||||
|
||||
# CT 103 (Vaultwarden) — каталог data
|
||||
pct exec 103 -- ls -la /opt/docker/vaultwarden/data
|
||||
pct exec 103 -- tar cf - -C /opt/docker/vaultwarden data | wc -c
|
||||
```
|
||||
|
||||
**Запуск из cron и доступ к Vaultwarden (bw):** В cron окружение ограничено: часто `PATH` не содержит `/usr/local/bin`, где обычно установлен `bw`. Скрипты дампов БД (ct101, ct104, ct103) в начале задают `export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"`, поэтому при запуске из cron `bw` и `jq` находятся без правки crontab. Нужно: 1) файл с мастер-паролем, например `/root/.bw-master` (chmod 600), и при необходимости переменная `BW_MASTER_PASSWORD_FILE=/root/.bw-master`; 2) один раз с интерактивной сессии: `bw config server https://vault.katykhin.ru`, `bw login` (сохранит сессию в конфиг); 3) при каждом запуске скрипт делает `bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw` и подставляет пароль БД. Если вручную дамп идёт, а из cron — нет, проверьте: наличие `/root/.bw-master`, права доступа, что `bw` доступен по этому PATH (запустите скрипт через `env -i PATH=/usr/local/bin:/usr/bin:/bin /root/scripts/backup-ct103-gitea-pgdump.sh` для имитации cron).
|
||||
|
||||
### Почему размер дампа меньше размера БД на диске
|
||||
|
||||
`pg_database_size()` показывает **размер БД на диске**: данные таблиц + **индексы** + TOAST (сжатые длинные значения) + свободное место и bloat. **pg_dump** выводит только логические данные в виде SQL: самих данных индексов в дампе нет (только команды `CREATE INDEX`), поэтому несжатый дамп часто **меньше** размера БД. После gzip сжатие даёт ещё примерно 2,5–4×. Итог: БД 2 GB на диске → несжатый дамп 200–600 MB → сжатый 50–200 MB нормален (особенно для Nextcloud с большими индексами по `oc_filecache`).
|
||||
|
||||
Если сомневаетесь, проверьте несжатый размер и число таблиц:
|
||||
|
||||
```bash
|
||||
# Несжатый размер дампа (на хосте)
|
||||
gunzip -c /mnt/backup/databases/ct101-nextcloud/nextcloud-db-YYYYMMDD-HHMM.sql.gz | wc -c
|
||||
|
||||
# Таблиц в дампе
|
||||
gunzip -c /mnt/backup/databases/ct101-nextcloud/nextcloud-db-YYYYMMDD-HHMM.sql.gz | grep -c '^CREATE TABLE '
|
||||
|
||||
# Таблиц в живой БД (в контейнере)
|
||||
pct exec 101 -- docker exec nextcloud-db-1 psql -U nextcloud -d nextcloud -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';"
|
||||
```
|
||||
|
||||
Числа таблиц должны совпадать. Несжатый размер для Nextcloud 2 GB на диске обычно 200–600 MB. При необходимости запустите бэкап с проверкой: `VERIFY_BACKUP=1 /root/scripts/backup-ct101-pgdump.sh` — скрипт выведет несжатый размер и число таблиц в дампе.
|
||||
|
||||
---
|
||||
|
||||
## Уведомления в Telegram
|
||||
|
||||
После **успешного** выполнения каждого бэкапа в Telegram отправляется короткое сообщение (заголовок с эмодзи + краткая сводка). Уведомления приходят по завершении соответствующего скрипта; для локального vzdump — по cron в **03:00** (проверка файлов за последние 2 часа).
|
||||
|
||||
|
||||
| Заголовок | Когда | Тело сообщения |
|
||||
|-----------|------|----------------|
|
||||
| 🖥️ VPS Миран | после 01:00 | Резервное копирование завершено. БД, voice_users, S3 (telegram-helper-bot). Размер копии: X. |
|
||||
| 🗄️ Nextcloud (БД) | после 01:15 | Резервное копирование завершено. Дамп БД Nextcloud. Размер: X. |
|
||||
| 📷 Фото Immich (rsync) | после 01:30 | Резервное копирование завершено. Библиотека фото синхронизирована. Размер: X. |
|
||||
| 🌐 VPS MTProto (DE) | после 01:45 | Резервное копирование завершено. Конфиги MTProto и сайт (VPS DE). Размер архива: X. |
|
||||
| 💾 Backup local | 03:00 | Резервное копирование завершено. Локальный vzdump (LXC/VM). Контейнеров/ВМ: N, объём: X ГБ. Время завершения: HH:MM. |
|
||||
| ⚙️ Конфиги хоста | после 02:15 | Резервное копирование завершено. Архивы /etc/pve и конфигов сети. Размер: X. |
|
||||
| 🗄️ Paperless (БД) | после 02:30 | Резервное копирование завершено. Дамп БД Paperless. Размер: X. |
|
||||
| 🔐 Vaultwarden | после 02:45 | Резервное копирование завершено. Данные Vaultwarden. Размер архива: X. |
|
||||
| 🗄️ Gitea (БД) | после 03:00 | Резервное копирование завершено. Дамп БД Gitea. Размер: X. |
|
||||
| 🗄️ Immich (БД) | после 03:15 | Резервное копирование завершено. Дамп БД Immich. Размер: X. |
|
||||
| 📐 Векторы RAG | после 03:30 | Резервное копирование завершено. Архив векторов RAG. Размер: X. |
|
||||
| ☁️ Restic Yandex | после 04:00 | Резервное копирование завершено. Снимок в Yandex: N файлов, размер X. |
|
||||
| 📷 Restic Yandex (photos) | после 04:10 | Резервное копирование завершено. Снимок фото в Yandex: N файлов, размер X. |
|
||||
|
||||
**Размер в сообщениях** — фактический размер файла (apparent size), а не занятое место на диске. Если видите **4.0K** или **8.0K** у дампа БД или архива Vaultwarden — копия может быть пустой или почти пустой (ошибка доступа к контейнеру, пустая БД, неверный путь). Скрипты при размере ниже порога (10 KB для дампов БД и Vaultwarden, 1 KB для MTProto) добавляют в сообщение строку: *«⚠️ Подозрительно малый размер — проверьте…»*. В этом случае проверьте на хосте: имя контейнера БД, путь к данным, логи скрипта.
|
||||
|
||||
**Единая точка отправки (шлюз):** скрипт **`/root/scripts/notify-telegram.sh`**. Все источники уведомлений вызывают только его и не обращаются к Telegram API напрямую. Токен и chat_id хранятся в одном конфиге на хосте Proxmox.
|
||||
|
||||
**Конфиг на хосте:** `/root/.telegram-notify.env` с переменными `TELEGRAM_BOT_TOKEN` и `TELEGRAM_CHAT_ID`. В репозитории лежит пример: **`scripts/telegram-notify.env.example`** — скопируйте его на хост в `/root/.telegram-notify.env` и подставьте свои значения:
|
||||
|
||||
```bash
|
||||
cp /path/to/scripts/telegram-notify.env.example /root/.telegram-notify.env
|
||||
chmod 600 /root/.telegram-notify.env
|
||||
```
|
||||
|
||||
**Как получить креды:**
|
||||
|
||||
1. **Токен бота:** в Telegram написать [@BotFather](https://t.me/BotFather), команда `/newbot`, следовать подсказкам — получите токен вида `123456789:ABCdef...`.
|
||||
2. **Chat ID:** отправить боту любое сообщение, затем в браузере открыть
|
||||
`https://api.telegram.org/bot<TOKEN>/getUpdates`
|
||||
В ответе в `updates[].message.chat.id` — ваш chat_id (число; для групп — отрицательное).
|
||||
|
||||
Если конфига или кредов нет, шлюз тихо выходит с 0 и не ломает вызывающие скрипты.
|
||||
|
||||
**Позже** тот же шлюз можно вызывать с VM 200 или с VPS (например по SSH на хост Proxmox) — отдельно не реализовано, архитектура это допускает.
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
@@ -229,3 +525,4 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н
|
||||
- [Стратегия бэкапов (фаза 1)](proxmox-phase1-backup.md) — общий план и принятые решения.
|
||||
- [Архитектура](../architecture/architecture.md) — хост, IP, доступ.
|
||||
- [VM 200 (Immich)](../containers/container-200.md) — сервисы, пути, .env.
|
||||
|
||||
|
||||
@@ -170,6 +170,122 @@ tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interface
|
||||
|
||||
Это не «шаг бэкапа», но обязательная часть восстановления: без паролей восстановленные контейнеры не войдут в сервисы.
|
||||
|
||||
**Инвентаризация секретов для переноса в Vaultwarden** — ниже сводная таблица: где лежат креды сейчас и какой объект в Vaultwarden им соответствует. Команды для получения значений из Vaultwarden и переключение скриптов — в разделе «Получение секретов из Vaultwarden» ниже.
|
||||
|
||||
| Хост / CT / VM | Текущее место | Объект Vaultwarden |
|
||||
|----------------|----------------|---------------------|
|
||||
| Proxmox (хост) | root, пользователи PVE | (в менеджере вручную) |
|
||||
| Proxmox (хост) | `/root/.restic-yandex.env`, `/root/.restic-password` | **RESTIC** (поля: RESTIC_BACKUP_KEY, RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_SECRET_ACCESS_KEY, TELEGRAM_SELF_CHAT_ID) |
|
||||
| Proxmox (хост) | `/root/.telegram-notify.env` | **HOME_BOT_TOKEN** (пароль = токен бота), **RESTIC** (поле TELEGRAM_SELF_CHAT_ID = chat_id) |
|
||||
| CT 100 | `/root/.secrets/certbot/beget.ini` | **beget** (логин, пароль) |
|
||||
| CT 100 | NPM админка | (в менеджере вручную) |
|
||||
| CT 100 | VPN Route Check compose | **localhost** (логин admin, пароль, поле ROUTER_TELNET_HOST) |
|
||||
| CT 100 | custom_ssl / letsencrypt | (восстановление из /etc/letsencrypt; в Vaultwarden не храним) |
|
||||
| CT 101 | Nextcloud compose, config.php | **NEXTCLOUD** (логин nextcloud, пароль; поля: NEXTCLOUD_TRUSTED_DOMAINS, instanceid, passwordsalt, secret, dbpassword) |
|
||||
| CT 103 | Gitea compose, .env, app.ini | **GITEA** (логин gitea, пароль; поля: GITEA__database__DB_TYPE, GITEA__database__HOST, GITEA_RUNNER_REGISTRATION_TOKEN, LFS_JWT_SECRET, INTERNAL_TOKEN) |
|
||||
| CT 103 | CouchDB local.ini | **OBSIDIAN** (логин obsidian, пароль) |
|
||||
| CT 103 | Vaultwarden .env | **VAULTWARDEN** (пароль = ADMIN_TOKEN, поле SIGNUPS_ALLOWED) |
|
||||
| CT 104 | Paperless compose, docker-compose.env | **PAPERLESS** (логин paperless, пароль; поля: PAPERLESS_URL, PAPERLESS_SECRET_KEY, PAPERLESS_TIME_ZONE, PAPERLESS_OCR_LANGUAGE, PAPERLESS_OCR_LANGUAGES) |
|
||||
| CT 107 | Invidious compose | **INVIDIOUS** (логин kemal, пароль; поля: SERVER_SECRET_KEY, test) |
|
||||
| CT 108 | ice-servers.json | **GALENE** (поле config — JSON TURN) |
|
||||
| VM 200 | `/opt/immich/.env` | **IMMICH** (логин/пароль и поля по .env) |
|
||||
| VM 200 | `/opt/immich-deduper/.env` | **IMMICH_DEDUPER** (логин postgres, пароль; поля: DEDUP_PORT, DEDUP_DATA, DEDUP_IMAGE, IMMICH_PATH, PSQL_HOST, PSQL_PORT, PSQL_DB) |
|
||||
| Proxmox (хост) | `/root/.vps-miran-s3.env` | **MIRAN_S3** (поля: S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME) |
|
||||
|
||||
---
|
||||
|
||||
#### Получение секретов из Vaultwarden
|
||||
|
||||
**Требования:** установлены `bw` (Bitwarden CLI) и `jq`; настроен сервер: `bw config server https://vault.katykhin.ru`. Мастер-пароль задаётся через переменную `BW_MASTER_PASSWORD` или через **файл с доступом только для текущего пользователя** (`chmod 600`), например `/root/.bw-master`; файл не хранить в репозитории. Перед запросами: `bw sync` и разблокировка: `export BW_SESSION=$(bw unlock --passwordenv BW_MASTER_PASSWORD --raw)` или `bw unlock --passwordfile /path/to/file --raw`.
|
||||
|
||||
**Команды по объектам** (выполнять после `bw unlock` в той же сессии):
|
||||
|
||||
| Объект | Логин / пароль | Кастомное поле |
|
||||
|--------|----------------|----------------|
|
||||
| **beget** | `bw get username "beget"`, `bw get password "beget"` | — |
|
||||
| **GALENE** | — | `bw get item "GALENE" \| jq -r '.fields[] \| select(.name=="config") \| .value'` |
|
||||
| **GITEA** | `bw get username "GITEA"`, `bw get password "GITEA"` | `bw get item "GITEA" \| jq -r '.fields[] \| select(.name=="ИМЯ_ПОЛЯ") \| .value'` — подставить GITEA_RUNNER_REGISTRATION_TOKEN, LFS_JWT_SECRET, INTERNAL_TOKEN, GITEA__database__DB_TYPE, GITEA__database__HOST |
|
||||
| **HOME_BOT_TOKEN** | — | пароль = токен: `bw get password "HOME_BOT_TOKEN"` |
|
||||
| **IMMICH** | по структуре в Vaultwarden | `bw get item "IMMICH" \| jq '.fields'` |
|
||||
| **IMMICH_DEDUPER** | `bw get username "IMMICH_DEDUPER"`, `bw get password "IMMICH_DEDUPER"` | поля DEDUP_*, IMMICH_PATH, PSQL_* — через `jq -r '.fields[] \| select(.name=="X") \| .value'` |
|
||||
| **INVIDIOUS** | `bw get username "INVIDIOUS"`, `bw get password "INVIDIOUS"` | `bw get item "INVIDIOUS" \| jq -r '.fields[] \| select(.name=="SERVER_SECRET_KEY") \| .value'` |
|
||||
| **localhost** | `bw get username "localhost"`, `bw get password "localhost"` | `bw get item "localhost" \| jq -r '.fields[] \| select(.name=="ROUTER_TELNET_HOST") \| .value'` |
|
||||
| **MIRAN_S3** | — | S3_ACCESS_KEY: `bw get item "MIRAN_S3" \| jq -r '.fields[] \| select(.name=="S3_ACCESS_KEY") \| .value'`; аналогично S3_SECRET_KEY, S3_BUCKET_NAME |
|
||||
| **NEXTCLOUD** | `bw get username "NEXTCLOUD"`, `bw get password "NEXTCLOUD"` | secret: `bw get item "NEXTCLOUD" \| jq -r '.fields[] \| select(.name=="secret") \| .value'`; dbpassword, passwordsalt, instanceid, NEXTCLOUD_TRUSTED_DOMAINS — то же с `.name=="..."` |
|
||||
| **OBSIDIAN** | `bw get username "OBSIDIAN"`, `bw get password "OBSIDIAN"` | — |
|
||||
| **PAPERLESS** | `bw get username "PAPERLESS"`, `bw get password "PAPERLESS"` | PAPERLESS_SECRET_KEY, PAPERLESS_URL и др. — `bw get item "PAPERLESS" \| jq -r '.fields[] \| select(.name=="PAPERLESS_SECRET_KEY") \| .value'` |
|
||||
| **RESTIC** | — | RESTIC_BACKUP_KEY: `bw get item "RESTIC" \| jq -r '.fields[] \| select(.name=="RESTIC_BACKUP_KEY") \| .value'`; RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, TELEGRAM_SELF_CHAT_ID — то же с нужным `.name` |
|
||||
| **VAULTWARDEN** | — | пароль = ADMIN_TOKEN: `bw get password "VAULTWARDEN"`; SIGNUPS_ALLOWED — из полей |
|
||||
|
||||
**Универсальный шаблон для поля по имени:**
|
||||
`bw get item "ИМЯ_ОБЪЕКТА" | jq -r '.fields[] | select(.name=="ИМЯ_ПОЛЯ") | .value'`
|
||||
|
||||
---
|
||||
|
||||
#### Переключение скриптов на секреты из Vaultwarden
|
||||
|
||||
Ниже — как перейти с чтения из файлов на подстановку из `bw` на **хосте Proxmox**. Мастер-пароль хранить **только в файле с доступом для текущего пользователя:** `chmod 600 /root/.bw-master` (владелец root — только root читает/пишет); в репозиторий файл не коммитить. Либо задавать переменную окружения при запуске по крону.
|
||||
|
||||
**1. Restic (backup-restic-yandex.sh, backup-restic-yandex-photos.sh, restore-one-vzdump-from-restic.sh)**
|
||||
|
||||
Сейчас: `source /root/.restic-yandex.env`, пароль из `/root/.restic-password`.
|
||||
|
||||
Переключение: в начале скрипта (после `set -e`) разблокировать BW и выставить переменные из объекта **RESTIC**:
|
||||
|
||||
```bash
|
||||
# Разблокировать Vaultwarden (мастер-пароль из файла с chmod 600 или env)
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw)
|
||||
fi
|
||||
# Подставить секреты из RESTIC
|
||||
ITEM=$(bw get item "RESTIC")
|
||||
export RESTIC_REPOSITORY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value')
|
||||
export AWS_ACCESS_KEY_ID=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value')
|
||||
export AWS_SECRET_ACCESS_KEY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value')
|
||||
export AWS_DEFAULT_REGION=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value')
|
||||
RESTIC_PASSWORD=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value')
|
||||
export RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||||
echo -n "$RESTIC_PASSWORD" > "$RESTIC_PASSWORD_FILE"
|
||||
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||||
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT
|
||||
```
|
||||
|
||||
Убрать из скрипта: проверку/чтение `ENV_FILE` и `RESTIC_PASSWORD_FILE` из файлов; оставить использование переменных `RESTIC_REPOSITORY`, `AWS_*`, `RESTIC_PASSWORD_FILE` как выше. Для restore-one-vzdump-from-restic.sh — тот же блок в начале.
|
||||
|
||||
**2. Telegram (notify-telegram.sh)**
|
||||
|
||||
Сейчас: `source /root/.telegram-notify.env`, переменные `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`.
|
||||
|
||||
Переключение: читать токен из **HOME_BOT_TOKEN** (пароль), chat_id из объекта **RESTIC** (поле TELEGRAM_SELF_CHAT_ID). В начале скрипта (если нет уже разблокировки):
|
||||
|
||||
```bash
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw)
|
||||
fi
|
||||
TELEGRAM_BOT_TOKEN=$(bw get password "HOME_BOT_TOKEN")
|
||||
TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value')
|
||||
```
|
||||
|
||||
Дальше в скрипте использовать `TELEGRAM_BOT_TOKEN` и `TELEGRAM_CHAT_ID` как раньше (проверка на пустоту, вызов curl). Файл `/root/.telegram-notify.env` можно не использовать.
|
||||
|
||||
**3. Дампы БД (backup-ct101-pgdump.sh, backup-ct104-pgdump.sh, backup-ct103-gitea-pgdump.sh)**
|
||||
|
||||
Скрипты уже берут PGPASSWORD из Vaultwarden: в начале разблокировка `bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw`, затем для pg_dump передаётся `-e PGPASSWORD=...` в `docker exec`. Источники паролей:
|
||||
|
||||
- **Nextcloud (CT 101):** объект **NEXTCLOUD** — поле `dbpassword` или пароль записи (`bw get password "NEXTCLOUD"`).
|
||||
- **Paperless (CT 104):** объект **PAPERLESS** — пароль (`bw get password "PAPERLESS"`).
|
||||
- **Gitea (CT 103):** объект **GITEA** — пароль (`bw get password "GITEA"`).
|
||||
|
||||
Требования на хосте: `bw`, для Nextcloud — `jq`; файл `/root/.bw-master` с мастер-паролем (chmod 600). При ошибке (дамп < 512 байт) скрипт завершается с кодом 1 и выводит stderr; уведомление в Telegram при ошибке не отправляется.
|
||||
|
||||
**4. Остальные места**
|
||||
|
||||
Конфиги сервисов (Nextcloud config.php, Gitea compose, Paperless .env, Immich .env и т.д.) подставлять вручную при восстановлении или написать небольшие скрипты-обёртки, которые один раз получают нужные значения через `bw get item` / `bw get password` и пишут в .env или конфиг. Имена объектов и полей — по таблице выше.
|
||||
|
||||
После переключения: обновить чек-лист (отметить «Секреты перенесены в Vaultwarden») и при необходимости добавить в cron установку `BW_MASTER_PASSWORD_FILE` или вызов разблокировки в общем wrapper’е.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 6. Тестовое восстановление одного контейнера
|
||||
@@ -213,14 +329,14 @@ tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interface
|
||||
- [x] Разметка: 1 ТБ на sdb1, ФС, монтирование в `/mnt/backup` (без LUKS). *(скрипт `scripts/backup-setup-sdb1-mount.sh`, каталоги созданы.)*
|
||||
- [x] В Proxmox добавлен Storage для VZDump → `/mnt/backup/proxmox/dump`.
|
||||
- [x] Настроена регулярная задача Backup: LXC (100–108), расписание ночь (02:00), retention задан. *VM 200 исключена из задания (образ ~380 ГБ); восстановление VM 200 — по инструкции «с нуля» в [backup-howto](backup-howto.md).*
|
||||
- [ ] Проверен ручной запуск Backup now — файлы появляются в storage. *(рекомендуется проверить разово.)*
|
||||
- [x] Настроен бэкап `/etc/pve` (скрипт + cron) → `/mnt/backup/proxmox/etc-pve`. *(backup-etc-pve.sh, 03:00, 30 дней.)*
|
||||
- [ ] Restic: cron на хосте, выгрузка нужных каталогов из `/mnt/backup` в Yandex S3, retention 7/4/6.
|
||||
- [ ] Yandex: ключи и endpoint зафиксированы, restic успешно пишет в бакет.
|
||||
- [x] Проверен ручной запуск Backup now — файлы появляются в storage. *(рекомендуется проверить разово.)*
|
||||
- [x] Настроен бэкап `/etc/pve` (скрипт + cron) → `/mnt/backup/proxmox/etc-pve`. *(backup-etc-pve.sh, 02:15, 30 дней.)*
|
||||
- [x] Restic: cron на хосте, выгрузка каталогов из `/mnt/backup` в Yandex S3. *(backup-restic-yandex.sh 04:00, backup-restic-yandex-photos.sh 04:10, retention 3 daily / 2 weekly / 2 monthly.)*
|
||||
- [x] Yandex: ключи и endpoint зафиксированы в `/root/.restic-yandex.env`, restic пишет в бакет.
|
||||
- [x] Vaultwarden развёрнут (CT 103).
|
||||
- [ ] Секреты перенесены в Vaultwarden. *(на усмотрение: root PVE, пароли БД, API и т.д.)*
|
||||
- [ ] Бэкап данных Vaultwarden включён в restic (Yandex S3). *Локально данные уже копируются в `/mnt/backup/other/vaultwarden/` (backup-vaultwarden-data.sh); при настройке restic — включить этот каталог в источники.*
|
||||
- [ ] Выполнено тестовое восстановление одного контейнера (другой VMID), проверена работоспособность.
|
||||
- [x] Бэкап данных Vaultwarden включён в restic (Yandex S3). *Локально: backup-vaultwarden-data.sh → `/mnt/backup/other/vaultwarden/`; restic выгружает весь `/mnt/backup` (кроме photos), каталог vaultwarden входит в снимок.*
|
||||
- [x] Выполнено тестовое восстановление одного контейнера (другой VMID), проверена работоспособность. *(26.02.2026: восстановлен CT 107 в слот 999 из `/mnt/backup/proxmox/dump/dump/vzdump-lxc-107-*.tar.zst`, проверены консоль, пинг, Docker, Invidious на 3000; тестовый CT удалён.)*
|
||||
- [x] В документации зафиксирована процедура полного восстановления Proxmox «с нуля». *[backup-howto.md](backup-howto.md): восстановление из vzdump, конфигов, БД, VM 200 с нуля, Vaultwarden, VPS и др.*
|
||||
|
||||
---
|
||||
@@ -229,6 +345,7 @@ tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interface
|
||||
|
||||
- [Архитектура и подключение](../architecture/architecture.md) — хосты, IP, домены.
|
||||
- [Схема сети и зависимости](../network/network-topology.md) — SPOF, зависимость от Proxmox и бэкапов.
|
||||
- [Vaultwarden и использование секретов](../vaultwarden-secrets.md) — установка bw, разблокировка, получение секретов в скриптах.
|
||||
- Документация контейнеров (100–108, 200) — бэкапы *данных внутри* сервисов (БД, тома); фаза 1 дополняет это бэкапом на уровне PVE.
|
||||
|
||||
---
|
||||
@@ -254,7 +371,7 @@ tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interface
|
||||
|
||||
## Осталось сделать
|
||||
|
||||
- **Yandex + Restic:** подготовить Static Key (Access Key + Secret), записать endpoint и имя бакета; настроить restic на хосте (cron): выгрузка `/mnt/backup` в Yandex S3, retention 3 daily / 2 weekly / 2 monthly. Скрипт: `scripts/backup-restic-yandex.sh`.
|
||||
- **Проверка:** один раз запустить Backup now в Proxmox UI и убедиться, что файлы появляются в storage.
|
||||
- **Тестовое восстановление:** восстановить один контейнер (например 105 или 107) под другим VMID, проверить доступ и сервисы, затем удалить тестовый.
|
||||
- **Секреты:** при необходимости перенести пароли/ключи (root PVE, БД, API) в Vaultwarden и обновлять при смене.
|
||||
- **Проверка ручного Backup:** один раз запустить «Backup now» в Proxmox UI (Datacenter → Backup) и убедиться, что файлы появляются в `/mnt/backup/proxmox/dump/dump/`.
|
||||
- **Секреты (по желанию):** перенести пароли/ключи (root PVE, БД, API) в Vaultwarden и обновлять при смене.
|
||||
|
||||
*Выполнено ранее: Yandex + Restic (cron, retention 3/2/2), тестовое восстановление CT 107 → 999 (26.02.2026).*
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
**Certbot на хосте (внутри CT 100):**
|
||||
- Установлен в системе, таймер `certbot.timer` (проверка продления дважды в день).
|
||||
- Учётные данные Beget API: `/root/.secrets/certbot/beget.ini`.
|
||||
- Deploy-hook’и: `/etc/letsencrypt/renewal-hooks/deploy/` — скрипты `copy-*-to-npm.sh` (video, docs, immich, mini-lm и т.д.) копируют `fullchain.pem` и `privkey.pem` в соответствующий каталог `custom_ssl/npm-<id>/` и делают `docker exec npm nginx -s reload`.
|
||||
- Deploy-hook’и: `/etc/letsencrypt/renewal-hooks/deploy/` — скрипты `copy-*-to-npm.sh` (video, docs, immich, mini-lm, vault и т.д.) копируют `fullchain.pem` и `privkey.pem` в соответствующий каталог `custom_ssl/npm-<id>/` и делают `docker exec npm nginx -s reload`.
|
||||
|
||||
**vault.katykhin.ru:** сертификат выпускается certbot’ом в `/etc/letsencrypt/live/vault.katykhin.ru/`, deploy-hook `copy-vault-to-npm.sh` копирует его в `custom_ssl/npm-18/`. В NPM у proxy host’а vault.katykhin.ru должен быть выбран именно этот сертификат (Custom SSL → каталог npm-18). Если в NPM по ошибке привязать другой сертификат (например от другого домена), браузер покажет ошибку «нет сертификата» или неверный домен; тогда в конфиге proxy host’а должны быть пути `ssl_certificate /data/custom_ssl/npm-18/...`.
|
||||
|
||||
Подробнее по SSL: [Выпуск сертификата Let's Encrypt (DNS-01)](../network/ssl-letsencrypt-dns01.md).
|
||||
|
||||
|
||||
@@ -16,6 +16,18 @@
|
||||
|
||||
---
|
||||
|
||||
## Как подключиться к серверу (CT 103)
|
||||
|
||||
- **С хоста Proxmox (192.168.1.150):**
|
||||
`pct exec 103 -- bash` — попадаете в shell контейнера 103 под root.
|
||||
- **По SSH (если настроен доступ на 103):**
|
||||
`ssh root@192.168.1.103` — с машины, с которой настроен ключ/пароль на 103.
|
||||
- **Логин в Debian:** `root`, пароль — из менеджера паролей или как задавали при установке.
|
||||
|
||||
После входа в CT 103 все команды (Docker, логи и т.д.) выполняются уже внутри контейнера.
|
||||
|
||||
---
|
||||
|
||||
## Доступ и логины
|
||||
|
||||
- **Debian (CT 103):** логин `root` (пароль — в менеджере паролей или как настраивал при установке).
|
||||
@@ -162,7 +174,7 @@ docker compose up -d
|
||||
**Доступ по домену (опционально):** если нужен **https://vault.katykhin.ru** и из LAN, и по VPN, в NPM (контейнер 100) настраивают:
|
||||
- **Proxy Host:** `vault.katykhin.ru` → upstream `192.168.1.103:8280`, включить SSL (Let's Encrypt или custom).
|
||||
- **Access List:** создать список, разрешающий только подсети **192.168.1.0/24** (LAN) и **10.10.99.0/24** (WireGuard VPN); для всех остальных — отказ. Эту access list привязать к proxy host `vault.katykhin.ru`. Тогда с интернета без VPN доступ к домену будет закрыт; из дома и по VPN — открыт.
|
||||
- В compose Vaultwarden при использовании домена задать `DOMAIN=https://vault.katykhin.ru` и перезапустить контейнер.
|
||||
- В compose Vaultwarden при использовании домена **обязательно** задать `DOMAIN=https://vault.katykhin.ru` и перезапустить контейнер. Если оставить `DOMAIN=http://192.168.1.103:8280`, веб-вход по https://vault.katykhin.ru может не работать (запрос prelogin падает, в DevTools — «Provisional headers»).
|
||||
|
||||
**Тома:**
|
||||
- `/opt/docker/vaultwarden/data` → `/data` (все данные Vaultwarden: база, вложения, и т.п.).
|
||||
@@ -190,6 +202,8 @@ curl -s http://127.0.0.1:8280/ | head -c 200
|
||||
|
||||
После этого интерфейс открывается по **`http://192.168.1.103:8280`** из домашней сети. Клиенты Bitwarden (ПК, телефон в LAN) настраивают на этот URL — сервис уже открыт в локальной сети без NPM. Если позже добавить домен в NPM (см. выше), в клиентах можно перейти на `https://vault.katykhin.ru`.
|
||||
|
||||
**Если веб-вход по https://vault.katykhin.ru не работает (prelogin ошибка, «Provisional headers» в DevTools):** проверьте, что в compose задано `DOMAIN=https://vault.katykhin.ru`. На CT 103: `grep DOMAIN /opt/docker/vaultwarden/docker-compose.yml`. Если там `DOMAIN=http://192.168.1.103:8280`, замените на `DOMAIN=https://vault.katykhin.ru`, затем `cd /opt/docker/vaultwarden && docker compose up -d` и повторите вход.
|
||||
|
||||
---
|
||||
|
||||
## Порты (сводка на хосте)
|
||||
@@ -271,3 +285,4 @@ curl -s http://127.0.0.1:8280/ | head -c 200
|
||||
|
||||
- [Архитектура и подключение](../architecture/architecture.md) — таблица контейнеров, домены (в т.ч. obsidian.katykhin.ru, home.katykhin.ru, wallos.katykhin.ru), схема сети.
|
||||
- [Контейнер 100 (nginx)](container-100.md) — NPM и AdGuard; через NPM проксируются git.katykhin.ru, obsidian.katykhin.ru, vault.katykhin.ru, home.katykhin.ru и wallos.katykhin.ru.
|
||||
- [Vaultwarden и использование секретов](../vaultwarden-secrets.md) — как получать пароли и поля из Vaultwarden через CLI (bw) в скриптах.
|
||||
|
||||
221
docs/vaultwarden-secrets.md
Normal file
221
docs/vaultwarden-secrets.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# Vaultwarden и использование секретов
|
||||
|
||||
Краткое руководство по **Vaultwarden** в homelab и по тому, как получать секреты из него в скриптах и при восстановлении.
|
||||
|
||||
---
|
||||
|
||||
## Что такое Vaultwarden
|
||||
|
||||
**Vaultwarden** — это self-hosted реализация API Bitwarden: менеджер паролей, совместимый с официальными клиентами Bitwarden (десктоп, мобильные приложения, браузерные расширения). Данные хранятся на вашем сервере, а не в облаке Bitwarden.
|
||||
|
||||
В нашей схеме Vaultwarden развёрнут на **контейнере 103** (Gitea). Доступ:
|
||||
|
||||
- **Веб:** https://vault.katykhin.ru (из LAN и по VPN; из интернета без VPN закрыт).
|
||||
- **По IP в LAN:** http://192.168.1.103:8280.
|
||||
|
||||
Подробнее про установку, порты и NPM — в [Контейнер 103 (Gitea, Vaultwarden)](containers/container-103.md#5-vaultwarden-менеджер-паролей).
|
||||
|
||||
**Зачем хранить секреты в Vaultwarden:**
|
||||
|
||||
- Один источник правды для паролей хоста, БД, API-ключей и т.д.
|
||||
- При восстановлении после сбоя не нужно искать креды по разным файлам.
|
||||
- Скрипты бэкапов и уведомлений могут брать секреты через Bitwarden CLI без хранения паролей в репозитории.
|
||||
|
||||
---
|
||||
|
||||
## Доступ к секретам: веб и CLI
|
||||
|
||||
- **Веб-интерфейс** — для ручного просмотра и редактирования записей (логины, пароли, кастомные поля). Вход по email и мастер-паролю.
|
||||
- **Bitwarden CLI (`bw`)** — для скриптов и командной строки: разблокировка хранилища, получение логина/пароля/полей по имени записи.
|
||||
|
||||
Далее в статье речь идёт в основном о **CLI**.
|
||||
|
||||
---
|
||||
|
||||
## Установка и настройка Bitwarden CLI (bw)
|
||||
|
||||
На машине, с которой нужно получать секреты (например, хост Proxmox), должны быть установлены **`bw`** и **`jq`**.
|
||||
|
||||
### Установка bw (Linux, вручную)
|
||||
|
||||
1. Скачать архив с [релизов Bitwarden CLI](https://github.com/bitwarden/cli/releases) (например `bw-linux-1.22.1.zip` для x86_64).
|
||||
2. Распаковать и положить бинарник в PATH, например:
|
||||
```bash
|
||||
unzip bw-linux-*.zip
|
||||
install -m 755 bw /usr/local/bin/bw
|
||||
```
|
||||
3. Установить `jq` (для разбора кастомных полей):
|
||||
```bash
|
||||
apt install jq # Debian/Proxmox
|
||||
```
|
||||
|
||||
### Настройка сервера и первый вход
|
||||
|
||||
1. Указать URL вашего Vaultwarden:
|
||||
```bash
|
||||
bw config server https://vault.katykhin.ru
|
||||
```
|
||||
2. Войти (интерактивно, один раз):
|
||||
```bash
|
||||
bw login
|
||||
```
|
||||
Ввести email и мастер-пароль от vault.katykhin.ru. Данные сессии сохранятся локально.
|
||||
|
||||
3. Проверить:
|
||||
```bash
|
||||
bw status
|
||||
bw sync
|
||||
```
|
||||
После ввода мастер-пароля (если хранилище было locked) синхронизация подтянет актуальные данные с сервера.
|
||||
|
||||
### Разблокировка для скриптов (файл с мастер-паролем)
|
||||
|
||||
В cron или в скриптах пароль вводить вручную нельзя. Используют **файл с мастер-паролем** с строгими правами:
|
||||
|
||||
```bash
|
||||
echo -n 'ВАШ_МАСТЕР_ПАРОЛЬ' > /root/.bw-master
|
||||
chmod 600 /root/.bw-master
|
||||
```
|
||||
|
||||
- Файл **не коммитить** в репозиторий и не копировать в открытые места.
|
||||
- Владелец — пользователь, под которым запускаются скрипты (например `root`); только он должен иметь доступ к файлу.
|
||||
|
||||
Проверка разблокировки без интерактивного ввода:
|
||||
|
||||
```bash
|
||||
bw unlock "$(cat /root/.bw-master)" --raw
|
||||
```
|
||||
|
||||
Команда должна вернуть длинную строку (session key). Эту строку скрипты передают в `BW_SESSION` (см. ниже).
|
||||
|
||||
---
|
||||
|
||||
## Как получать секреты из Vaultwarden
|
||||
|
||||
### Состояние и синхронизация
|
||||
|
||||
- **`bw status`** — показать URL сервера, последнюю синхронизацию, email пользователя и состояние: `unlocked` / `locked`.
|
||||
- **`bw sync`** — обновить локальный кэш с сервера (при необходимости перед чтением актуальных данных).
|
||||
|
||||
Если хранилище **locked**, перед любыми `bw get ...` нужно разблокировать:
|
||||
|
||||
```bash
|
||||
export BW_SESSION=$(bw unlock --passwordfile /root/.bw-master --raw)
|
||||
```
|
||||
|
||||
Дальше в этой же сессии (пока переменная `BW_SESSION` экспортирована) можно вызывать `bw get ...`.
|
||||
|
||||
### Логин и пароль записи
|
||||
|
||||
Для записей типа «логин» (Login) в Vaultwarden:
|
||||
|
||||
- **Логин (username):**
|
||||
`bw get username "ИМЯ_ЗАПИСИ"`
|
||||
- **Пароль:**
|
||||
`bw get password "ИМЯ_ЗАПИСИ"`
|
||||
|
||||
Примеры: `bw get password "GITEA"`, `bw get username "PAPERLESS"`. Имя записи — то, как она называется в веб-интерфейсе (чувствительно к регистру).
|
||||
|
||||
### Кастомные поля (custom fields)
|
||||
|
||||
В Bitwarden/Vaultwarden у записи могут быть **кастомные поля** (например `RESTIC_REPOSITORY`, `TELEGRAM_SELF_CHAT_ID`). Они не выводятся через `bw get username/password`, их достают через **`bw get item`** и **`jq`**:
|
||||
|
||||
```bash
|
||||
bw get item "ИМЯ_ЗАПИСИ" | jq -r '.fields[] | select(.name=="ИМЯ_ПОЛЯ") | .value'
|
||||
```
|
||||
|
||||
Примеры:
|
||||
|
||||
- Поле `RESTIC_BACKUP_KEY` из записи **RESTIC:**
|
||||
`bw get item "RESTIC" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value'`
|
||||
- Поле `TELEGRAM_SELF_CHAT_ID` из **RESTIC:**
|
||||
`bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value'`
|
||||
|
||||
Полный JSON записи (все поля):
|
||||
`bw get item "ИМЯ_ЗАПИСИ" | jq '.'`
|
||||
|
||||
---
|
||||
|
||||
## Использование в скриптах
|
||||
|
||||
### Общий подход
|
||||
|
||||
1. В начале скрипта (если ещё не разблокировано) прочитать мастер-пароль из файла и разблокировать:
|
||||
```bash
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw)
|
||||
fi
|
||||
```
|
||||
2. При необходимости выполнить `bw sync`.
|
||||
3. Получить нужные значения через `bw get username`, `bw get password`, `bw get item ... | jq ...` и присвоить переменным окружения или использовать в командах.
|
||||
|
||||
Переменная **`BW_SESSION`** передаётся в дочерние процессы, поэтому все вызовы `bw` в том же процессе и в дочерних скриптах будут видеть разблокированное хранилище.
|
||||
|
||||
### Пример: Restic (репозиторий и ключ из Vaultwarden)
|
||||
|
||||
```bash
|
||||
# Разблокировать (мастер-пароль из файла с chmod 600)
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw)
|
||||
fi
|
||||
|
||||
ITEM=$(bw get item "RESTIC")
|
||||
export RESTIC_REPOSITORY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value')
|
||||
export AWS_ACCESS_KEY_ID=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value')
|
||||
export AWS_SECRET_ACCESS_KEY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value')
|
||||
export AWS_DEFAULT_REGION=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value')
|
||||
RESTIC_PASSWORD=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value')
|
||||
export RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||||
echo -n "$RESTIC_PASSWORD" > "$RESTIC_PASSWORD_FILE"
|
||||
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||||
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT
|
||||
|
||||
# Дальше: restic backup ... и т.д.
|
||||
```
|
||||
|
||||
Пароль restic в файле — временный; `trap` удаляет его по выходу из скрипта.
|
||||
|
||||
### Пример: Telegram (токен и chat_id из Vaultwarden)
|
||||
|
||||
Токен бота хранится в записи **HOME_BOT_TOKEN** (пароль = токен); chat_id — в записи **RESTIC**, поле `TELEGRAM_SELF_CHAT_ID`:
|
||||
|
||||
```bash
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw)
|
||||
fi
|
||||
TELEGRAM_BOT_TOKEN=$(bw get password "HOME_BOT_TOKEN")
|
||||
TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value')
|
||||
# Дальше: curl к Telegram API с TELEGRAM_BOT_TOKEN и TELEGRAM_CHAT_ID
|
||||
```
|
||||
|
||||
### Fallback на старые конфиги
|
||||
|
||||
Если Vaultwarden недоступен или разблокировка не удалась, скрипты могут загружать креды из прежних файлов (например `/root/.telegram-notify.env`, `/root/.restic-yandex.env`). Так можно обеспечить работу бэкапов даже при временной недоступности vault.
|
||||
|
||||
---
|
||||
|
||||
## Безопасность
|
||||
|
||||
- **Файл с мастер-паролем:** только владелец (например root), права `chmod 600`. Не хранить в git и не копировать на общие ресурсы.
|
||||
- **Переменная BW_SESSION:** не логировать и не выводить в скриптах; не передавать в ненадёжные процессы.
|
||||
- **Временные файлы с паролями** (как RESTIC_PASSWORD_FILE выше): создавать с `chmod 600`, удалять по завершении (`trap ... EXIT`).
|
||||
- **Бэкап данных Vaultwarden:** каталог `/opt/docker/vaultwarden/data` на CT 103 входит в план бэкапов (restic → Yandex), см. [backup-howto](backup/backup-howto.md). Без этого при потере сервера теряется и хранилище паролей.
|
||||
|
||||
---
|
||||
|
||||
## Инвентаризация записей и полей
|
||||
|
||||
В Vaultwarden удобно хранить записи с именами, совпадающими с сервисами: **RESTIC**, **GITEA**, **PAPERLESS**, **NEXTCLOUD**, **HOME_BOT_TOKEN**, **VAULTWARDEN**, **MIRAN_S3** и т.д. У записей типа «логин» — логин/пароль; у записей с множеством значений — кастомные поля (например `RESTIC_REPOSITORY`, `AWS_ACCESS_KEY_ID`).
|
||||
|
||||
Полная таблица «где лежат креды сейчас → какой объект в Vaultwarden» и готовые команды `bw get ...` / `jq` по каждому объекту описаны в [Фаза 1: Стратегия бэкапов](backup/proxmox-phase1-backup.md) — разделы «Инвентаризация секретов для переноса в Vaultwarden», «Получение секретов из Vaultwarden» и «Переключение скриптов на секреты из Vaultwarden».
|
||||
|
||||
---
|
||||
|
||||
## См. также
|
||||
|
||||
- [Контейнер 103 (Gitea, Vaultwarden)](containers/container-103.md) — развёртывание Vaultwarden, порты, домен, NPM.
|
||||
- [Фаза 1: Стратегия бэкапов](backup/proxmox-phase1-backup.md) — инвентаризация секретов, команды по объектам, переключение скриптов на Vaultwarden.
|
||||
- [backup-howto](backup/backup-howto.md) — общий план бэкапов и восстановления, в том числе данных Vaultwarden.
|
||||
@@ -92,4 +92,12 @@ ss -ulnp | grep 33118
|
||||
|
||||
Параметры обфускации на обоих серверах (Германия и США) **одинаковые** — конфиг можно полностью перенести на новый сервер при переезде. На роутере создаётся второе VPN-подключение; переключение между Germany и USA — выбор нужного профиля.
|
||||
|
||||
→ **Подробно:** [Перенос конфигурации AmneziaWG между серверами](vpn-migrate-config.md).
|
||||
→ **Подробно:** [Перенос конфигурации AmneziaWG между серверами](vpn-migrate-config.md).
|
||||
|
||||
---
|
||||
|
||||
## MTProto и сайт-заглушка (план)
|
||||
|
||||
На этом же VPS можно развернуть MTProto proxy с маскировкой под обычный HTTPS-сайт (один порт 443): при заходе на домен — статический сайт, при подключении Telegram с секретом — прокси. Домен: katykhin.store. VPN (AmneziaWG) при этом остаётся на порту 33118/UDP без изменений.
|
||||
|
||||
→ **План развёртывания:** [MTProto + сайт на VPS Германия (план)](vpn-vps-mtproto-site-plan.md).
|
||||
|
||||
112
docs/vps/vpn-vps-mtproto-check-report.md
Normal file
112
docs/vps/vpn-vps-mtproto-check-report.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Отчёт проверки MTProto + маскировка (katykhin.store)
|
||||
|
||||
Проверки выполнены **с внешней консоли** (без SSH на сервер) 2026-02-26. Цель: убедиться, что на 443 выглядит как обычный HTTPS, а MTProto срабатывает только при наличии секрета.
|
||||
|
||||
---
|
||||
|
||||
## 1) HTTPS сайта
|
||||
|
||||
**Команда:** `curl -I https://katykhin.store/`
|
||||
|
||||
**Результат:**
|
||||
- **HTTP/1.1 200 OK**
|
||||
- Server: nginx/1.24.0 (Ubuntu)
|
||||
- Content-Type: text/html
|
||||
|
||||
**Вывод:** Сайт отдаётся по **HTTPS**, не по HTTP. Маскировка по TLS выполнена.
|
||||
|
||||
---
|
||||
|
||||
## 2) Порт 443 и TLS
|
||||
|
||||
### Порт 443 открыт
|
||||
|
||||
**Команда:** `nc -vz katykhin.store 443`
|
||||
|
||||
**Результат:** `Connection to katykhin.store port 443 [tcp/https] succeeded!`
|
||||
|
||||
### TLS-рукопожатие и сертификат
|
||||
|
||||
**Команда:** `openssl s_client -connect katykhin.store:443 -servername katykhin.store`
|
||||
|
||||
**Результат:**
|
||||
- **Verification: OK**
|
||||
- **Сертификат:** Let's Encrypt (E8), CN=katykhin.store
|
||||
- **Срок:** NotBefore 26 Feb 2026, NotAfter 27 May 2026
|
||||
- **Протокол:** TLSv1.3, Cipher TLS_AES_256_GCM_SHA384
|
||||
- **SAN:** host "katykhin.store" в сертификате
|
||||
|
||||
**Вывод:** Внешний вид — обычный HTTPS с валидным доверенным сертификатом, без самоподписи.
|
||||
|
||||
---
|
||||
|
||||
## 3) Однострочная проверка
|
||||
|
||||
**Команда:** `curl -Iv https://katykhin.store/`
|
||||
|
||||
**Результат:**
|
||||
- TLS handshake успешен
|
||||
- Сертификат проверен (SSL certificate verify ok)
|
||||
- **HTTP/1.1 200 OK**
|
||||
- subject: CN=katykhin.store, issuer: Let's Encrypt
|
||||
|
||||
**Вывод:** Важнейшая часть маскировки выполнена: **katykhin.store:443 = обычный HTTPS**.
|
||||
|
||||
---
|
||||
|
||||
## 4) Поведение при «не-TLS» подключении к 443
|
||||
|
||||
### Сырой TCP без данных
|
||||
|
||||
**Команда:** подключение к 443 без отправки данных (nc без ввода).
|
||||
|
||||
**Результат:** Ответа нет (сервер ждёт данные). mtg перенаправил соединение на nginx:993; nginx ждёт TLS ClientHello — логично.
|
||||
|
||||
### Plain HTTP на порт 443
|
||||
|
||||
**Команда:** отправка `GET / HTTP/1.0` на 443 (без TLS).
|
||||
|
||||
**Результат:** Ответ от nginx:
|
||||
- **HTTP/1.1 400 Bad Request**
|
||||
- «The plain HTTP request was sent to HTTPS port»
|
||||
|
||||
**Вывод:** На 443 приходит трафик без MTProto-секрета → mtg отдаёт его nginx (cloak). Nginx на 993 принимает только TLS, поэтому на plain HTTP отвечает 400. Для DPI/активной проверки, которые делают **нормальный TLS** (как браузер или `openssl s_client`), будет обычный HTTPS и страница сайта. Plain HTTP на 443 даёт только 400 от nginx, а не MTProto.
|
||||
|
||||
---
|
||||
|
||||
## 5) Итоговая таблица поведения
|
||||
|
||||
| Подключение | Ожидание по плану | Факт |
|
||||
|--------------------------|--------------------------|-------------------------------|
|
||||
| Браузер / curl HTTPS | Сайт-заглушка, 200 | ✅ HTTP/1.1 200, nginx, TLS |
|
||||
| TLS (openssl s_client) | Валидный HTTPS | ✅ Let's Encrypt, TLS 1.3 |
|
||||
| Telegram с MTProto | Работа прокси | ✅ Подтверждено пользователем |
|
||||
| Plain HTTP на 443 | Не MTProto | ✅ 400 от nginx (HTTPS port) |
|
||||
| Соединение без данных | Нет «сырого» HTTP-ответа| ✅ Нет ответа (ожидание TLS) |
|
||||
|
||||
---
|
||||
|
||||
## 6) Что не проверялось из консоли (нужен сервер или устройство)
|
||||
|
||||
- **Логи nginx** (access.log / error.log) — нужен SSH.
|
||||
- **Логи mtg** (принятые соединения с секретом) — нужен SSH.
|
||||
- **Проверка из РФ** (iPhone/устройство в целевой сети, без общего VPN, с MTProto) — нужно выполнить у тебя.
|
||||
- **Симуляция активного probing** уже по сути сделана: `openssl s_client` к 443 ведёт себя как обычный HTTPS-клиент и получает нормальный TLS и сайт.
|
||||
|
||||
---
|
||||
|
||||
## 7) Частые ошибки — статус
|
||||
|
||||
| Риск | Статус |
|
||||
|-----------------------------|--------|
|
||||
| Сайт только по HTTP | ❌ Нет: HTTPS отвечает 200 |
|
||||
| Самоподписанный сертификат | ❌ Нет: Let's Encrypt |
|
||||
| MTProto на нестандартном порту | ❌ Нет: используется 443 |
|
||||
| Не-MTProto видит прокси | ❌ Нет: без секрета — nginx/сайт или 400 |
|
||||
|
||||
---
|
||||
|
||||
## Краткий вывод
|
||||
|
||||
**Сеть видит katykhin.store:443 как обычный HTTPS** (валидный сертификат, TLS 1.3, ответ 200 на GET /).
|
||||
Только клиент с правильным MTProto-секретом попадает на прокси; остальное идёт в nginx (сайт или 400 на plain HTTP). Маскировка настроена корректно.
|
||||
172
docs/vps/vpn-vps-mtproto-site-plan.md
Normal file
172
docs/vps/vpn-vps-mtproto-site-plan.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# План: MTProto + сайт-заглушка на VPS Германия (один порт 443)
|
||||
|
||||
Развёртывание MTProto proxy с TLS-camouflage и статическим сайтом на одном порту 443 на VPS в Германии (185.103.253.99). VPN (AmneziaWG) остаётся на текущем кастомном порту без изменений.
|
||||
|
||||
---
|
||||
|
||||
## Цель
|
||||
|
||||
- **Один порт 443:** при заходе по HTTPS на домен — отдаётся обычный сайт; при подключении Telegram с секретом — трафик идёт в MTProto. Для DPI/проверок сервер выглядит как обычный веб-сайт.
|
||||
- **Домен:** katykhin.store (A-запись на 185.103.253.99).
|
||||
- **VPN:** без изменений, порт 33118/UDP (AmneziaWG), текущие конфиги клиентов и роутера не трогаем.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
Клиент (браузер) → 443 → mtg → (нет секрета) → nginx:993 → статический сайт
|
||||
Клиент (Telegram + proxy) → 443 → mtg → (есть секрет) → MTProto → Telegram DC
|
||||
```
|
||||
|
||||
- **mtg** слушает 0.0.0.0:443. По первым байтам определяет: если передан корректный dd-secret — обрабатывает как MTProto; иначе перенаправляет соединение на **cloak-port** (nginx на 993).
|
||||
- **Nginx** слушает 127.0.0.1:993 (или 0.0.0.0:993) с TLS, отдаёт статический сайт по Let's Encrypt для katykhin.store.
|
||||
- **AmneziaWG** — как сейчас, порт 33118/UDP, конфигурация не меняется.
|
||||
|
||||
Выбор **mtg** (а не nginx stream + официальный MTProxy): один компонент на 443, встроенная логика «секрет → MTProto, иначе → cloak», меньше точек отказа и проще отладка. Избегаем связки обфускация + MTProxy на одном сервере.
|
||||
|
||||
---
|
||||
|
||||
## Как это работает
|
||||
|
||||
### Потоки трафика
|
||||
|
||||
- **Обычный HTTPS (браузер / curl):**
|
||||
- Клиент устанавливает TLS‑соединение на `katykhin.store:443`.
|
||||
- `mtg` принимает соединение, не видит корректного MTProto‑секрета и прокидывает его на nginx по `localhost:993` (cloak‑порт).
|
||||
- Nginx завершает TLS, отдаёт статический сайт (`/var/www/katykhin.store`), сертификат — Let's Encrypt для `katykhin.store`.
|
||||
|
||||
- **Telegram с MTProto‑proxy (с правильным секретом):**
|
||||
- Клиент шлёт fake‑TLS трафик с секретом (`ee…`).
|
||||
- `mtg` распознаёт секрет и работает как MTProto‑прокси: устанавливает соединение с Telegram DC и пересылает трафик через себя.
|
||||
- Для внешнего наблюдателя подключение всё равно идёт на `katykhin.store:443` по TLS.
|
||||
|
||||
- **Неверный/отсутствующий секрет, “чужой” клиент или plain HTTP на 443:**
|
||||
- Соединение уходит в nginx на 993.
|
||||
- TLS‑клиенты (обычный `openssl s_client`, браузер) получают нормальный сертификат и ответ 200.
|
||||
- Plain HTTP на 443 получает от nginx стандартный `400 The plain HTTP request was sent to HTTPS port`.
|
||||
|
||||
### Роль отдельных компонентов
|
||||
|
||||
- **mtg (fake TLS / obfuscated secret):**
|
||||
- Слушает `0.0.0.0:443`.
|
||||
- Секрет сгенерирован как `mtg generate-secret -c katykhin.store tls` (префикс `ee…`, fake TLS).
|
||||
- Если секрет корректный — MTProto; если нет — прокидка на nginx (cloak‑порт).
|
||||
|
||||
- **nginx:**
|
||||
- Слушает `993/tcp` с TLS, работает только как backend для mtg.
|
||||
- Для любого валидного TLS‑клиента выглядит как обычный сайт с Let’s Encrypt‑сертификатом.
|
||||
|
||||
- **AmneziaWG:**
|
||||
- Продолжает слушать `33118/udp` на VPS, используется для VPN‑туннеля и никак не завязан на MTProto/HTTPS.
|
||||
|
||||
---
|
||||
|
||||
## Маскировка и безопасность
|
||||
|
||||
- **TLS‑маскировка:**
|
||||
- Сертификат: Let’s Encrypt, CN=`katykhin.store`, TLS 1.3.
|
||||
- При проверке через `curl -Iv` и `openssl s_client` сервер ведёт себя как одиночный нормальный HTTPS‑хост.
|
||||
- Никаких самоподписанных сертификатов или нестандартных портов.
|
||||
|
||||
- **Fake TLS / obfuscated secret:**
|
||||
- Используется `mtg` с режимом fake TLS, секрет вида `ee…`.
|
||||
- Без секрета MTProto не “торчит наружу”: active probing с обычным TLS видит только сайт или стандартную ошибку HTTPS‑порта.
|
||||
|
||||
- **Firewall (ufw):**
|
||||
- Входящие разрешены только:
|
||||
- `22/tcp` — SSH.
|
||||
- `80/tcp` — HTTP (для выдачи/продления сертификатов).
|
||||
- `443/tcp` — MTProto+HTTPS (mtg).
|
||||
- `33118/udp` — AmneziaWG.
|
||||
- `993/tcp` не открыт наружу — им пользуется только mtg локально.
|
||||
|
||||
- **Rate limiting и fail2ban:**
|
||||
- В nginx включён лимит запросов и соединений на сервере `katykhin.store` (cloak‑порт 993):
|
||||
- `limit_req_zone` ~ 10 r/s на IP, `burst=20`.
|
||||
- `limit_conn` ~ 10 одновременных соединений на IP.
|
||||
- `fail2ban`:
|
||||
- jail `sshd` — защита от перебора паролей по SSH.
|
||||
- jail `nginx-limit-req` настроен, но в текущей схеме почти не срабатывает (см. примечание ниже).
|
||||
|
||||
**Важно:** запросы к nginx на 993 приходят от mtg (`127.0.0.1`), поэтому в логах nginx реальные клиентские IP не видны, и `nginx-limit-req` практически не пригоден для блокировки внешних адресов. Основная практическая защита здесь — rate limiting самого nginx и jail `sshd` в `fail2ban`.
|
||||
|
||||
---
|
||||
|
||||
## Использование
|
||||
|
||||
### MTProto‑proxy
|
||||
|
||||
- **Параметры прокси:**
|
||||
- Сервер: `katykhin.store` (или напрямую IP `185.103.253.99`).
|
||||
- Порт: `443`.
|
||||
- Секрет: `eecf027cbd3a05d5ff99a18796a51958516b6174796b68696e2e73746f7265`.
|
||||
|
||||
- **Готовые ссылки:**
|
||||
- Через t.me:
|
||||
`https://t.me/proxy?server=katykhin.store&port=443&secret=eecf027cbd3a05d5ff99a18796a51958516b6174796b68696e2e73746f7265`
|
||||
- Через tg:// по IP:
|
||||
`tg://proxy?server=185.103.253.99&port=443&secret=eecf027cbd3a05d5ff99a18796a51958516b6174796b68696e2e73746f7265`
|
||||
|
||||
- **Ожидаемое поведение:**
|
||||
- При включении прокси Telegram устанавливает соединение на `katykhin.store:443`.
|
||||
- Секрет распознаётся mtg, трафик уходит в Telegram DC.
|
||||
- Снаружи это выглядит как обычный HTTPS‑трафик к сайту.
|
||||
|
||||
### VPN (AmneziaWG)
|
||||
|
||||
- Конфигурация VPN не менялась:
|
||||
- Сервер: `185.103.253.99`.
|
||||
- Порт: `33118/udp`.
|
||||
- Все существующие клиенты/роутер продолжают использовать старые конфиги.
|
||||
|
||||
MTProto и VPN живут независимо: MTProto занимает 443/tcp, AmneziaWG — 33118/udp.
|
||||
|
||||
---
|
||||
|
||||
## Мониторинг и отладка (на уже развёрнутой схеме)
|
||||
|
||||
- **mtg:**
|
||||
- Текущий статус сервиса: `systemctl status mtg`.
|
||||
- Живые логи: `journalctl -u mtg -f`.
|
||||
|
||||
- **nginx:**
|
||||
- Доступы/ошибки по сайту:
|
||||
`tail -f /var/log/nginx/access.log`
|
||||
`tail -f /var/log/nginx/error.log`
|
||||
|
||||
- **fail2ban:**
|
||||
- Общий статус: `fail2ban-client status`.
|
||||
- jail `sshd`: `fail2ban-client status sshd`.
|
||||
- Разбан IP: `fail2ban-client set sshd unbanip <IP>`.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Итоговая схема портов
|
||||
|
||||
| Порт | Служба | Доступ |
|
||||
|-------------|-------------|---------------|
|
||||
| 22/tcp | SSH | интернет |
|
||||
| 80/tcp | HTTP | для certbot |
|
||||
| 443/tcp | mtg | интернет |
|
||||
| 993/tcp | nginx (SSL) | только localhost (для mtg cloak) |
|
||||
| 33118/udp | AmneziaWG | интернет |
|
||||
|
||||
---
|
||||
|
||||
## Ссылки и ссылка для Telegram
|
||||
|
||||
- Документация mtg: https://github.com/9seconds/mtg
|
||||
- Руководство с cloak: https://v2how.github.io/post/2021-02-18-camouflage-telegram-mtproto-proxy-ubuntu-20-04/
|
||||
- Ссылка для подключения к прокси (развёрнуто 2026-02-26):
|
||||
`https://t.me/proxy?server=katykhin.store&port=443&secret=eecf027cbd3a05d5ff99a18796a51958516b6174796b68696e2e73746f7265`
|
||||
Альтернативно по IP: `tg://proxy?server=185.103.253.99&port=443&secret=eecf027cbd3a05d5ff99a18796a51958516b6174796b68696e2e73746f7265`
|
||||
|
||||
---
|
||||
|
||||
## Связь с другими документами
|
||||
|
||||
- Текущий VPN и доступ к VPS: [VPN-сервер (VPS, AmneziaWG)](vpn-vps-amneziawg.md).
|
||||
- После выполнения плана имеет смысл добавить в `vpn-vps-amneziawg.md` краткий раздел «MTProto и сайт» со ссылкой на этот документ и указанием домена/порта.
|
||||
@@ -3,6 +3,8 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec (SSH не нужен).
|
||||
# Результат: /mnt/backup/databases/ct101-nextcloud/nextcloud-db-YYYYMMDD-HHMM.sql.gz
|
||||
set -e
|
||||
# Чтобы из cron находились bw и jq (часто в /usr/local/bin)
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=101
|
||||
BACKUP_DIR="/mnt/backup/databases/ct101-nextcloud"
|
||||
@@ -16,18 +18,56 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/nextcloud-db-$DATE.sql.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- docker exec "$PG_CONTAINER" pg_dump -U nextcloud nextcloud 2>/dev/null | gzip > "$OUTPUT"
|
||||
# PGPASSWORD из Vaultwarden (объект NEXTCLOUD: поле dbpassword или пароль). См. docs/backup/proxmox-phase1-backup.md
|
||||
PG_ENV_ARGS=""
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true
|
||||
if [ -n "${BW_SESSION:-}" ]; then
|
||||
PGPASS=$(bw get item "NEXTCLOUD" 2>/dev/null | jq -r '.fields[] | select(.name=="dbpassword") | .value')
|
||||
[ -z "$PGPASS" ] && PGPASS=$(bw get password "NEXTCLOUD" 2>/dev/null)
|
||||
[ -n "$PGPASS" ] && PG_ENV_ARGS="-e PGPASSWORD=$PGPASS"
|
||||
fi
|
||||
fi
|
||||
pct exec $CT_ID -- docker exec $PG_ENV_ARGS "$PG_CONTAINER" pg_dump -U nextcloud nextcloud 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
# Проверка: несжатый размер дампа (2GB БД на диске → 200–600MB SQL нормально: индексы не в дампе, потом gzip)
|
||||
if [ "${VERIFY_BACKUP:-0}" = "1" ]; then
|
||||
UNCOMPRESSED=$(gunzip -c "$OUTPUT" 2>/dev/null | wc -c)
|
||||
UNCOMPRESSED_MB=$(( UNCOMPRESSED / 1024 / 1024 ))
|
||||
echo "Несжатый размер дампа: ${UNCOMPRESSED_MB} MB (${UNCOMPRESSED} B)"
|
||||
TABLES_IN_DUMP=$(gunzip -c "$OUTPUT" 2>/dev/null | grep -c '^CREATE TABLE ' || true)
|
||||
echo "Таблиц в дампе: $TABLES_IN_DUMP"
|
||||
fi
|
||||
else
|
||||
echo "Ошибка: дамп пустой или контейнер недоступен."
|
||||
echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (NEXTCLOUD)."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'nextcloud-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: дамп БД Nextcloud (PostgreSQL).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте контейнер nextcloud-db-1 и наличие данных в БД."
|
||||
"$NOTIFY_SCRIPT" "🗄️ Nextcloud (БД)" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||
# Результат: /mnt/backup/databases/ct103-gitea/gitea-db-YYYYMMDD-HHMM.sql.gz
|
||||
set -e
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=103
|
||||
BACKUP_DIR="/mnt/backup/databases/ct103-gitea"
|
||||
@@ -14,18 +15,47 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/gitea-db-$DATE.sql.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- docker exec "$PG_CONTAINER" pg_dump -U gitea gitea 2>/dev/null | gzip > "$OUTPUT"
|
||||
# PGPASSWORD из Vaultwarden (объект GITEA, пароль). См. docs/backup/proxmox-phase1-backup.md
|
||||
PG_ENV_ARGS=""
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true
|
||||
if [ -n "${BW_SESSION:-}" ]; then
|
||||
PGPASS=$(bw get password "GITEA" 2>/dev/null)
|
||||
[ -n "$PGPASS" ] && PG_ENV_ARGS="-e PGPASSWORD=$PGPASS"
|
||||
fi
|
||||
fi
|
||||
pct exec $CT_ID -- docker exec $PG_ENV_ARGS "$PG_CONTAINER" pg_dump -U gitea gitea 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: дамп пустой или контейнер недоступен."
|
||||
echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (GITEA)."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'gitea-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: дамп БД Gitea (PostgreSQL).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте контейнер gitea-db-1 и наличие данных в БД."
|
||||
"$NOTIFY_SCRIPT" "🗄️ Gitea (БД)" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||
# Результат: /mnt/backup/databases/ct104-paperless/paperless-db-YYYYMMDD-HHMM.sql.gz
|
||||
set -e
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=104
|
||||
BACKUP_DIR="/mnt/backup/databases/ct104-paperless"
|
||||
@@ -14,18 +15,47 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/paperless-db-$DATE.sql.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- docker exec "$PG_CONTAINER" pg_dump -U paperless paperless 2>/dev/null | gzip > "$OUTPUT"
|
||||
# PGPASSWORD из Vaultwarden (объект PAPERLESS, пароль). См. docs/backup/proxmox-phase1-backup.md
|
||||
PG_ENV_ARGS=""
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true
|
||||
if [ -n "${BW_SESSION:-}" ]; then
|
||||
PGPASS=$(bw get password "PAPERLESS" 2>/dev/null)
|
||||
[ -n "$PGPASS" ] && PG_ENV_ARGS="-e PGPASSWORD=$PGPASS"
|
||||
fi
|
||||
fi
|
||||
pct exec $CT_ID -- docker exec $PG_ENV_ARGS "$PG_CONTAINER" pg_dump -U paperless paperless 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: дамп пустой или контейнер недоступен."
|
||||
echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (PAPERLESS)."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'paperless-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: дамп БД Paperless (PostgreSQL).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте контейнер paperless-db-1 и наличие данных в БД."
|
||||
"$NOTIFY_SCRIPT" "🗄️ Paperless (БД)" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||
# Результат: /mnt/backup/other/ct105-vectors/vectors-YYYYMMDD-HHMM.tar.gz
|
||||
set -e
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=105
|
||||
REMOTE_PATH="/home/rag-service/data/vectors"
|
||||
BACKUP_DIR="/mnt/backup/other/ct105-vectors"
|
||||
RETENTION_DAYS=14
|
||||
|
||||
# Минимальный размер архива (байт). Пустой gzip ≈ 20 байт — каталог пуст или путь неверный.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
@@ -17,15 +20,31 @@ fi
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/vectors-$DATE.tar.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- tar cf - -C /home/rag-service/data vectors 2>/dev/null | gzip > "$OUTPUT"
|
||||
pct exec $CT_ID -- tar cf - -C /home/rag-service/data vectors 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: архив пустой или каталог недоступен."
|
||||
echo "Ошибка: архив пустой или слишком мал (${SIZE_BYTES} байт). Проверьте /home/rag-service/data/vectors в CT $CT_ID."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'vectors-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: архив векторов RAG (CT 105, vectors.npz).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте /home/rag-service/data/vectors в CT 105."
|
||||
"$NOTIFY_SCRIPT" "📐 Векторы RAG" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -21,3 +21,12 @@ chmod 600 "$BACKUP_ROOT"/etc-pve-*.tar.gz "$BACKUP_ROOT"/etc-host-configs-*.tar.
|
||||
|
||||
find "$BACKUP_ROOT" -name 'etc-pve-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
find "$BACKUP_ROOT" -name 'etc-host-configs-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du -sh "$BACKUP_ROOT" 2>/dev/null | cut -f1) || true
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: архивы /etc/pve, конфиги сети (interfaces, hosts, resolv.conf).
|
||||
Размер копии: ${SIZE:-—}."
|
||||
"$NOTIFY_SCRIPT" "⚙️ Конфиги хоста" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Копирование библиотеки фото Immich (оригиналы) с VM 200 на диск бэкапов хоста.
|
||||
# Запускать на хосте Proxmox под root. Требуется SSH без пароля: root → admin@192.168.1.200.
|
||||
# Запускать на хосте Proxmox под root. Требуется: SSH без пароля root → admin@192.168.1.200; на VM 200 установлен rsync (apt install rsync).
|
||||
# Результат: /mnt/backup/photos/library/ (зеркало /mnt/data/library с VM 200).
|
||||
# Без --delete: удалённые в Immich фото в бэкапе остаются (страховка).
|
||||
set -e
|
||||
@@ -19,3 +19,12 @@ mkdir -p "$BACKUP_PATH"
|
||||
rsync -az --timeout=3600 \
|
||||
--exclude=".stfolder" \
|
||||
"$VM_SSH:$REMOTE_PATH/" "$BACKUP_PATH/"
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du -sh "$BACKUP_PATH" 2>/dev/null | cut -f1) || true
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: библиотека фото Immich (rsync с VM 200).
|
||||
Размер копии: ${SIZE:-—}."
|
||||
"$NOTIFY_SCRIPT" "📷 Фото Immich (rsync)" "$BODY" || true
|
||||
fi
|
||||
|
||||
111
scripts/backup-restic-yandex-photos.sh
Normal file
111
scripts/backup-restic-yandex-photos.sh
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# Выгрузка только /mnt/backup/photos в Yandex Object Storage (S3) через restic.
|
||||
# Тот же репозиторий, что и backup-restic-yandex.sh; фото вынесены в отдельный снимок (больше всего данных).
|
||||
# Запускать на хосте Proxmox под root. Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600).
|
||||
# Cron: 10 4 * * * (04:10, после основного restic в 04:00).
|
||||
set -e
|
||||
|
||||
BACKUP_PATH="/mnt/backup/photos"
|
||||
# Время запуска (для логов и уведомлений)
|
||||
START_TS=$(date +%s)
|
||||
START_HUMAN=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Секреты из Vaultwarden (объект RESTIC)
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden."
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION
|
||||
BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || { echo "Не удалось разблокировать Vaultwarden."; exit 1; }
|
||||
RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || { echo "Не найден объект RESTIC в Vaultwarden."; exit 1; }
|
||||
export RESTIC_REPOSITORY
|
||||
RESTIC_REPOSITORY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value')
|
||||
export AWS_ACCESS_KEY_ID
|
||||
AWS_ACCESS_KEY_ID=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value')
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
AWS_SECRET_ACCESS_KEY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value')
|
||||
export AWS_DEFAULT_REGION
|
||||
AWS_DEFAULT_REGION=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value')
|
||||
[ -z "$AWS_DEFAULT_REGION" ] && AWS_DEFAULT_REGION="ru-central1"
|
||||
RESTIC_PASS=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value')
|
||||
for var in RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
|
||||
if [ -z "${!var}" ]; then
|
||||
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||||
echo -n "$RESTIC_PASS" > "$RESTIC_PASSWORD_FILE"
|
||||
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||||
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT INT TERM
|
||||
export RESTIC_PASSWORD_FILE
|
||||
|
||||
if ! command -v restic >/dev/null 2>&1; then
|
||||
echo "restic не установлен. Установите: apt install restic."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$BACKUP_PATH" ]; then
|
||||
echo "Каталог $BACKUP_PATH не найден. Пропуск."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Restic backup (photos): $BACKUP_PATH -> $RESTIC_REPOSITORY"
|
||||
# Показываем прогресс restic (без --quiet), чтобы был виден ход бэкапа
|
||||
restic backup "$BACKUP_PATH"
|
||||
|
||||
echo "Restic forget (retention 3 daily, 2 weekly, 2 monthly)..."
|
||||
restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet
|
||||
|
||||
echo "Restic prune..."
|
||||
restic prune --quiet
|
||||
|
||||
# Время окончания и длительность
|
||||
END_TS=$(date +%s)
|
||||
END_HUMAN=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
DURATION_SEC=$(( END_TS - START_TS ))
|
||||
if [ "$DURATION_SEC" -lt 0 ] 2>/dev/null; then
|
||||
DURATION_SEC=0
|
||||
fi
|
||||
DUR_MIN=$(( DURATION_SEC / 60 ))
|
||||
DUR_SEC=$(( DURATION_SEC % 60 ))
|
||||
|
||||
echo "Restic photos backup done."
|
||||
echo "Время запуска: $START_HUMAN"
|
||||
echo "Время завершения: $END_HUMAN"
|
||||
echo "Длительность: ${DUR_MIN} мин ${DUR_SEC} сек"
|
||||
|
||||
# Уведомление в Telegram (шлюз тихо выходит, если конфига нет)
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
STATS=$(restic stats latest 2>/dev/null) || true
|
||||
FILES=$(echo "$STATS" | grep "Total File Count" | sed 's/.*:[[:space:]]*//')
|
||||
SIZE=$(echo "$STATS" | grep "Total Size" | sed 's/.*:[[:space:]]*//')
|
||||
if [ -n "$FILES" ] && [ -n "$SIZE" ]; then
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: снимок /mnt/backup/photos в Yandex. Файлов в снимке: $FILES.
|
||||
Размер копии: ${SIZE}.
|
||||
Время запуска: ${START_HUMAN}.
|
||||
Время завершения: ${END_HUMAN}.
|
||||
Длительность: ${DUR_MIN} мин ${DUR_SEC} сек."
|
||||
"$NOTIFY_SCRIPT" "📷 Restic Yandex (photos)" "$BODY" || true
|
||||
else
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: снимок /mnt/backup/photos в Yandex.
|
||||
Размер копии: — (stats недоступны).
|
||||
Время запуска: ${START_HUMAN}.
|
||||
Время завершения: ${END_HUMAN}.
|
||||
Длительность: ${DUR_MIN} мин ${DUR_SEC} сек."
|
||||
"$NOTIFY_SCRIPT" "📷 Restic Yandex (photos)" "$BODY" || true
|
||||
fi
|
||||
fi
|
||||
@@ -1,56 +1,67 @@
|
||||
#!/bin/bash
|
||||
# Выгрузка /mnt/backup в Yandex Object Storage (S3) через restic.
|
||||
# Выгрузка /mnt/backup в Yandex Object Storage (S3) через restic (без каталога photos).
|
||||
# Фото бэкапятся отдельно: backup-restic-yandex-photos.sh.
|
||||
# Запускать на хосте Proxmox под root.
|
||||
# Перед первым запуском: установить restic, создать /root/.restic-yandex.env и /root/.restic-password, выполнить restic init.
|
||||
# Секреты: из Vaultwarden (объект RESTIC). Требуется файл с мастер-паролем: /root/.bw-master (chmod 600).
|
||||
# Перед первым запуском: установить restic, bw (Bitwarden CLI), jq; bw config server https://vault.katykhin.ru; restic init.
|
||||
# Cron: 0 4 * * * (04:00, после окна 01:00–03:30; 05:00 зарезервировано под перезагрузку).
|
||||
set -e
|
||||
|
||||
ENV_FILE="/root/.restic-yandex.env"
|
||||
BACKUP_PATH="/mnt/backup"
|
||||
# Исключаем служебные каталоги
|
||||
EXCLUDE_OPTS=(--exclude="$BACKUP_PATH/lost+found")
|
||||
# Время запуска (для логов и уведомлений)
|
||||
START_TS=$(date +%s)
|
||||
START_HUMAN=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
# Исключаем служебные каталоги и photos (фото — отдельный бэкап)
|
||||
EXCLUDE_OPTS=(--exclude="$BACKUP_PATH/lost+found" --exclude="$BACKUP_PATH/photos")
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи."
|
||||
# Секреты из Vaultwarden (объект RESTIC)
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$ENV_FILE"
|
||||
|
||||
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden."
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION
|
||||
BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || { echo "Не удалось разблокировать Vaultwarden. Проверьте мастер-пароль и доступ к vault.katykhin.ru."; exit 1; }
|
||||
RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || { echo "Не найден объект RESTIC в Vaultwarden."; exit 1; }
|
||||
export RESTIC_REPOSITORY
|
||||
RESTIC_REPOSITORY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value')
|
||||
export AWS_ACCESS_KEY_ID
|
||||
AWS_ACCESS_KEY_ID=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value')
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
AWS_SECRET_ACCESS_KEY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value')
|
||||
export AWS_DEFAULT_REGION
|
||||
AWS_DEFAULT_REGION=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value')
|
||||
[ -z "$AWS_DEFAULT_REGION" ] && AWS_DEFAULT_REGION="ru-central1"
|
||||
RESTIC_PASS=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value')
|
||||
for var in RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
|
||||
if [ -z "${!var}" ]; then
|
||||
echo "В $ENV_FILE не задано: $var"
|
||||
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$RESTIC_PASSWORD_FILE" ]; then
|
||||
RESTIC_PASSWORD_FILE="/root/.restic-password"
|
||||
fi
|
||||
if [ ! -f "$RESTIC_PASSWORD_FILE" ]; then
|
||||
echo "Файл с паролем репозитория не найден: $RESTIC_PASSWORD_FILE. Создайте его и выполните restic init."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export AWS_ACCESS_KEY_ID
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||||
echo -n "$RESTIC_PASS" > "$RESTIC_PASSWORD_FILE"
|
||||
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||||
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT INT TERM
|
||||
export RESTIC_PASSWORD_FILE
|
||||
export RESTIC_REPOSITORY
|
||||
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-ru-central1}"
|
||||
|
||||
if ! command -v restic >/dev/null 2>&1; then
|
||||
echo "restic не установлен. Установите: apt install restic."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Restic backup: $BACKUP_PATH -> $RESTIC_REPOSITORY"
|
||||
restic backup "$BACKUP_PATH" "${EXCLUDE_OPTS[@]}" --quiet
|
||||
echo "Restic backup: $BACKUP_PATH (excl. photos) -> $RESTIC_REPOSITORY"
|
||||
# Показываем прогресс restic (без --quiet), чтобы был виден ход бэкапа
|
||||
restic backup "$BACKUP_PATH" "${EXCLUDE_OPTS[@]}"
|
||||
|
||||
echo "Restic forget (retention 3 daily, 2 weekly, 2 monthly)..."
|
||||
restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet
|
||||
@@ -58,4 +69,42 @@ restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet
|
||||
echo "Restic prune..."
|
||||
restic prune --quiet
|
||||
|
||||
# Время окончания и длительность
|
||||
END_TS=$(date +%s)
|
||||
END_HUMAN=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
DURATION_SEC=$(( END_TS - START_TS ))
|
||||
if [ "$DURATION_SEC" -lt 0 ] 2>/dev/null; then
|
||||
DURATION_SEC=0
|
||||
fi
|
||||
DUR_MIN=$(( DURATION_SEC / 60 ))
|
||||
DUR_SEC=$(( DURATION_SEC % 60 ))
|
||||
|
||||
echo "Restic backup done."
|
||||
echo "Время запуска: $START_HUMAN"
|
||||
echo "Время завершения: $END_HUMAN"
|
||||
echo "Длительность: ${DUR_MIN} мин ${DUR_SEC} сек"
|
||||
|
||||
# Уведомление в Telegram (шлюз тихо выходит, если конфига нет)
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
STATS=$(restic stats latest 2>/dev/null) || true
|
||||
FILES=$(echo "$STATS" | grep "Total File Count" | sed 's/.*:[[:space:]]*//')
|
||||
SIZE=$(echo "$STATS" | grep "Total Size" | sed 's/.*:[[:space:]]*//')
|
||||
if [ -n "$FILES" ] && [ -n "$SIZE" ]; then
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: снимок /mnt/backup в Yandex (без photos). Файлов в снимке: $FILES.
|
||||
Размер копии: ${SIZE}.
|
||||
Время запуска: ${START_HUMAN}.
|
||||
Время завершения: ${END_HUMAN}.
|
||||
Длительность: ${DUR_MIN} мин ${DUR_SEC} сек."
|
||||
"$NOTIFY_SCRIPT" "☁️ Restic Yandex" "$BODY" || true
|
||||
else
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: снимок /mnt/backup в Yandex (без photos).
|
||||
Размер копии: — (stats недоступны).
|
||||
Время запуска: ${START_HUMAN}.
|
||||
Время завершения: ${END_HUMAN}.
|
||||
Длительность: ${DUR_MIN} мин ${DUR_SEC} сек."
|
||||
"$NOTIFY_SCRIPT" "☁️ Restic Yandex" "$BODY" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||
# Результат: /mnt/backup/other/vaultwarden/vaultwarden-data-YYYYMMDD-HHMM.tar.gz
|
||||
set -e
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=103
|
||||
REMOTE_PATH="/opt/docker/vaultwarden/data"
|
||||
@@ -15,19 +16,38 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Минимальный размер архива (байт). Пустой tar.gz ≈ 20 байт — значит каталог пуст или путь неверный.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/vaultwarden-data-$DATE.tar.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- tar cf - -C /opt/docker/vaultwarden data 2>/dev/null | gzip > "$OUTPUT"
|
||||
pct exec $CT_ID -- tar cf - -C /opt/docker/vaultwarden data 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
chmod 600 "$OUTPUT"
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: архив пустой или каталог недоступен."
|
||||
echo "Ошибка: архив пустой или слишком мал (${SIZE_BYTES} байт). Проверьте путь /opt/docker/vaultwarden/data в CT $CT_ID."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'vaultwarden-data-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: данные Vaultwarden (пароли).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте /opt/docker/vaultwarden/data в CT 103."
|
||||
"$NOTIFY_SCRIPT" "🔐 Vaultwarden" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -30,7 +30,7 @@ else
|
||||
fi
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: дамп пустой или не создан."
|
||||
rm -f "$OUTPUT"
|
||||
@@ -39,3 +39,15 @@ fi
|
||||
|
||||
# Удалить дампы старше RETENTION_DAYS
|
||||
find "$BACKUP_DIR" -name 'immich-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: дамп БД Immich (PostgreSQL, VM 200).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте скрипт на VM 200 и наличие данных в БД."
|
||||
"$NOTIFY_SCRIPT" "🗄️ Immich (БД)" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -71,3 +71,12 @@ if [ -f "$S3_ENV" ]; then
|
||||
else
|
||||
echo "Подсказка: для бэкапа S3 создайте $S3_ENV с S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME."
|
||||
fi
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du -sh "$BACKUP_ROOT" 2>/dev/null | cut -f1) || true
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: БД, voice_users, S3 (telegram-helper-bot).
|
||||
Размер копии: ${SIZE:-—}."
|
||||
"$NOTIFY_SCRIPT" "🖥️ VPS Миран" "$BODY" || true
|
||||
fi
|
||||
|
||||
54
scripts/backup-vps-mtproto.sh
Normal file
54
scripts/backup-vps-mtproto.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Бэкап конфигов MTProto + сайт-заглушка с VPS Германия (185.103.253.99).
|
||||
# Запускать на хосте Proxmox под root.
|
||||
# Требуется: SSH без пароля с хоста к root@185.103.253.99 (ключ в ~/.ssh).
|
||||
# Результат: /mnt/backup/vps/mtproto-germany/mtproto-config-YYYYMMDD-HHMM.tar.gz
|
||||
set -e
|
||||
|
||||
VPS_HOST="185.103.253.99"
|
||||
VPS_USER="root"
|
||||
SSH_OPTS=(-o ConnectTimeout=15 -o BatchMode=yes)
|
||||
BACKUP_ROOT="/mnt/backup/vps/mtproto-germany"
|
||||
RETENTION_DAYS=14
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$BACKUP_ROOT"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
ARCHIVE="$BACKUP_ROOT/mtproto-config-$DATE.tar.gz"
|
||||
|
||||
# Проверка доступа к VPS
|
||||
if ! ssh "${SSH_OPTS[@]}" "${VPS_USER}@${VPS_HOST}" "echo ok" >/dev/null 2>&1; then
|
||||
echo "Ошибка: нет доступа по SSH к ${VPS_USER}@${VPS_HOST}. Настройте ключ: ssh-copy-id ${VPS_USER}@${VPS_HOST}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Архив с VPS: mtg, nginx, letsencrypt (katykhin.store), статика сайта
|
||||
ssh "${SSH_OPTS[@]}" "${VPS_USER}@${VPS_HOST}" "tar -chzf - -C / \
|
||||
etc/systemd/system/mtg.service \
|
||||
etc/nginx/sites-available \
|
||||
etc/nginx/sites-enabled \
|
||||
etc/letsencrypt/live/katykhin.store \
|
||||
etc/letsencrypt/archive/katykhin.store \
|
||||
etc/letsencrypt/renewal/katykhin.store.conf \
|
||||
var/www/katykhin.store" > "$ARCHIVE"
|
||||
|
||||
chmod 600 "$ARCHIVE"
|
||||
echo "Бэкап MTProto (VPS DE): $ARCHIVE ($(du --apparent-size -h "$ARCHIVE" | cut -f1))"
|
||||
|
||||
find "$BACKUP_ROOT" -name 'mtproto-config-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$ARCHIVE" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$ARCHIVE" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: конфиги MTProto, nginx, Let's Encrypt, сайт (VPS DE).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 1024 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте SSH и наличие файлов на VPS."
|
||||
"$NOTIFY_SCRIPT" "🌐 VPS MTProto (DE)" "$BODY" || true
|
||||
fi
|
||||
63
scripts/notify-telegram.sh
Normal file
63
scripts/notify-telegram.sh
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# Единая точка отправки уведомлений в Telegram (шлюз).
|
||||
# Вызывают скрипты бэкапов на хосте Proxmox. Позже тот же шлюз можно вызывать с VM 200 / VPS по SSH.
|
||||
# Использование: notify-telegram.sh "Заголовок" "Текст сообщения"
|
||||
# Секреты: из Vaultwarden (токен — пароль объекта HOME_BOT_TOKEN, chat_id — поле TELEGRAM_SELF_CHAT_ID объекта RESTIC).
|
||||
# Файл с мастер-паролем: /root/.bw-master (chmod 600). Если его нет — тихо выходим с 0, не ломаем вызывающий скрипт.
|
||||
|
||||
set -e
|
||||
|
||||
TITLE="${1:-Notification}"
|
||||
BODY="${2:-}"
|
||||
|
||||
# Креды из Vaultwarden или из старого конфига (fallback)
|
||||
TELEGRAM_BOT_TOKEN=""
|
||||
TELEGRAM_CHAT_ID=""
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
|
||||
BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true
|
||||
if [ -n "$BW_SESSION" ]; then
|
||||
export BW_SESSION
|
||||
TELEGRAM_BOT_TOKEN=$(bw get password "HOME_BOT_TOKEN" 2>/dev/null) || true
|
||||
RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || true
|
||||
if [ -n "$RESTIC_ITEM" ]; then
|
||||
TELEGRAM_CHAT_ID=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value' 2>/dev/null) || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
||||
ENV_FILE="${TELEGRAM_NOTIFY_ENV:-/root/.telegram-notify.env}"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$ENV_FILE"
|
||||
fi
|
||||
fi
|
||||
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "$BODY" ]; then
|
||||
TEXT="$TITLE"
|
||||
else
|
||||
TEXT="$TITLE
|
||||
|
||||
$BODY"
|
||||
fi
|
||||
|
||||
URL="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||
if [ -n "${TELEGRAM_DEBUG:-}" ]; then
|
||||
curl -s -w "\nHTTP_CODE:%{http_code}\n" -X POST "$URL" \
|
||||
--data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
|
||||
--data-urlencode "text=$TEXT" \
|
||||
-d "disable_web_page_preview=true" \
|
||||
--max-time 10
|
||||
else
|
||||
curl -sf -X POST "$URL" \
|
||||
--data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
|
||||
--data-urlencode "text=$TEXT" \
|
||||
-d "disable_web_page_preview=true" \
|
||||
--max-time 10 \
|
||||
>/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
48
scripts/notify-vzdump-success.sh
Normal file
48
scripts/notify-vzdump-success.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# Проверяет каталог локальных vzdump за последние 2 часа и отправляет в Telegram сводку.
|
||||
# Задание Proxmox Backup выполняется в 02:00; этот скрипт запускают по cron в 03:00.
|
||||
# Использование: notify-vzdump-success.sh [путь_к_dump]
|
||||
# По умолчанию: /mnt/backup/proxmox/dump/dump/
|
||||
|
||||
DUMP_DIR="${1:-/mnt/backup/proxmox/dump/dump}"
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
# Файлы, изменённые за последние 120 минут (2 часа)
|
||||
MAX_AGE_MIN=120
|
||||
|
||||
if [ ! -d "$DUMP_DIR" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -x "$NOTIFY_SCRIPT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Список файлов vzdump, изменённых за последние MAX_AGE_MIN минут
|
||||
RECENT=$(find "$DUMP_DIR" -maxdepth 1 -type f \( -name 'vzdump-*.tar.zst' -o -name 'vzdump-*.vma.zst' -o -name 'vzdump-*.vma' \) -mmin "-$MAX_AGE_MIN" 2>/dev/null)
|
||||
|
||||
if [ -z "$RECENT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
COUNT=$(echo "$RECENT" | grep -c . 2>/dev/null || echo 0)
|
||||
[ "$COUNT" -eq 0 ] && exit 0
|
||||
|
||||
TOTAL_BYTES=$(echo "$RECENT" | while read -r f; do stat -c %s "$f" 2>/dev/null; done | awk '{s+=$1} END {print s+0}')
|
||||
[ -z "$TOTAL_BYTES" ] && TOTAL_BYTES=0
|
||||
|
||||
# Размер в ГБ (округление до 2 знаков; если bc нет — целое число)
|
||||
TOTAL_GB=$(echo "scale=2; $TOTAL_BYTES / 1024 / 1024 / 1024" | bc 2>/dev/null)
|
||||
[ -z "$TOTAL_GB" ] && TOTAL_GB="$((TOTAL_BYTES / 1024 / 1024 / 1024))"
|
||||
|
||||
# Время последнего изменения (последний записанный файл = время завершения бэкапа)
|
||||
LATEST_MTIME=$(echo "$RECENT" | while read -r f; do stat -c %Y "$f" 2>/dev/null; done | sort -n | tail -1)
|
||||
FINISH_TIME=""
|
||||
[ -n "$LATEST_MTIME" ] && FINISH_TIME=$(date -d "@$LATEST_MTIME" +%H:%M 2>/dev/null) || true
|
||||
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: локальный vzdump (LXC/VM). Контейнеров/ВМ: $COUNT.
|
||||
Размер копии: ${TOTAL_GB} ГБ."
|
||||
[ -n "$FINISH_TIME" ] && BODY="${BODY}
|
||||
Время завершения: ${FINISH_TIME}."
|
||||
"$NOTIFY_SCRIPT" "💾 Backup local" "$BODY" || true
|
||||
exit 0
|
||||
127
scripts/restore-one-vzdump-from-restic.sh
Normal file
127
scripts/restore-one-vzdump-from-restic.sh
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
# Восстановление одного файла vzdump из restic (Yandex S3) через mount.
|
||||
# Не выкачивает весь репозиторий — подгружаются только нужные данные для выбранного файла.
|
||||
# Запускать на хосте Proxmox под root. Требуется FUSE (restic mount).
|
||||
# Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600).
|
||||
#
|
||||
# Использование:
|
||||
# restore-one-vzdump-from-restic.sh [SNAPSHOT] [ПУТЬ_В_СНИМКЕ] [КУДА_СОХРАНИТЬ]
|
||||
#
|
||||
# Пример (последний снимок, CT 107, сохранить в /mnt/backup):
|
||||
# ./restore-one-vzdump-from-restic.sh
|
||||
# ./restore-one-vzdump-from-restic.sh latest /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-2026_02_26-02_03_14.tar.zst /mnt/backup
|
||||
#
|
||||
# Список снимков: restic snapshots
|
||||
# Список файлов в снимке: restic ls SNAPSHOT
|
||||
set -e
|
||||
|
||||
MOUNT_DIR="${MOUNT_DIR:-/mnt/backup/restic-mount}"
|
||||
|
||||
SNAPSHOT="${1:-latest}"
|
||||
# Путь к файлу внутри снимка (как в restic ls) — бэкапим /mnt/backup, пути вида /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-...
|
||||
FILE_IN_SNAPSHOT="${2:-}"
|
||||
OUTPUT_DIR="${3:-/mnt/backup}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Секреты из Vaultwarden (объект RESTIC)
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden."
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION
|
||||
BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || { echo "Не удалось разблокировать Vaultwarden."; exit 1; }
|
||||
RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || { echo "Не найден объект RESTIC в Vaultwarden."; exit 1; }
|
||||
export RESTIC_REPOSITORY
|
||||
RESTIC_REPOSITORY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value')
|
||||
export AWS_ACCESS_KEY_ID
|
||||
AWS_ACCESS_KEY_ID=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value')
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
AWS_SECRET_ACCESS_KEY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value')
|
||||
export AWS_DEFAULT_REGION
|
||||
AWS_DEFAULT_REGION=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value')
|
||||
[ -z "$AWS_DEFAULT_REGION" ] && AWS_DEFAULT_REGION="ru-central1"
|
||||
RESTIC_PASS=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value')
|
||||
for var in RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
|
||||
if [ -z "${!var}" ]; then
|
||||
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||||
echo -n "$RESTIC_PASS" > "$RESTIC_PASSWORD_FILE"
|
||||
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||||
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT INT TERM
|
||||
export RESTIC_PASSWORD_FILE
|
||||
|
||||
if ! command -v restic >/dev/null 2>&1; then
|
||||
echo "restic не установлен. Установите: apt install restic."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$FILE_IN_SNAPSHOT" ]; then
|
||||
echo "Использование: $0 [SNAPSHOT] ПУТЬ_В_СНИМКЕ [КУДА_СОХРАНИТЬ]"
|
||||
echo "Пример: $0 latest /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-2026_02_26-02_03_14.tar.zst /mnt/backup"
|
||||
echo ""
|
||||
echo "Доступные снимки:"
|
||||
restic snapshots 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve "latest" to snapshot ID (restic mount показывает ids/<short-id>/)
|
||||
if [ "$SNAPSHOT" = "latest" ]; then
|
||||
SNAPSHOT_ID=$(restic snapshots 2>/dev/null | grep -E '^[a-f0-9]{8}[[:space:]]' | tail -1 | awk '{print $1}')
|
||||
else
|
||||
SNAPSHOT_ID="$SNAPSHOT"
|
||||
fi
|
||||
[ -z "$SNAPSHOT_ID" ] && echo "Не удалось определить ID снимка." && exit 1
|
||||
echo "Снимок: $SNAPSHOT_ID"
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
mkdir -p "$MOUNT_DIR"
|
||||
|
||||
# Проверяем, не смонтировано ли уже
|
||||
if mountpoint -q "$MOUNT_DIR" 2>/dev/null; then
|
||||
echo "Точка монтирования $MOUNT_DIR уже занята. Размонтируйте: fusermount -u $MOUNT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Монтируем репозиторий в $MOUNT_DIR ..."
|
||||
restic mount "$MOUNT_DIR" &
|
||||
MOUNT_PID=$!
|
||||
trap 'rm -f "$RESTIC_PASSWORD_FILE"; kill $MOUNT_PID 2>/dev/null; fusermount -u "$MOUNT_DIR" 2>/dev/null; exit' EXIT INT TERM
|
||||
|
||||
# В смонтированном репо файлы снимка лежат в ids/<short_id>/<путь>
|
||||
SOURCE_FILE="$MOUNT_DIR/ids/$SNAPSHOT_ID/$FILE_IN_SNAPSHOT"
|
||||
for i in $(seq 1 45); do
|
||||
if [ -e "$SOURCE_FILE" ] 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ ! -e "$SOURCE_FILE" ]; then
|
||||
echo "Файл не найден: $SOURCE_FILE (подождали 45 с). Проверьте: restic ls $SNAPSHOT_ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASENAME_FILE="$(basename "$FILE_IN_SNAPSHOT")"
|
||||
OUTPUT_FILE="$OUTPUT_DIR/$BASENAME_FILE"
|
||||
echo "Копируем $SOURCE_FILE -> $OUTPUT_FILE ..."
|
||||
cp "$SOURCE_FILE" "$OUTPUT_FILE"
|
||||
echo "Готово: $OUTPUT_FILE ($(du -sh "$OUTPUT_FILE" | cut -f1))"
|
||||
|
||||
# Размонтировать
|
||||
kill $MOUNT_PID 2>/dev/null || true
|
||||
wait $MOUNT_PID 2>/dev/null || true
|
||||
fusermount -u "$MOUNT_DIR" 2>/dev/null || true
|
||||
trap - EXIT INT TERM
|
||||
echo "Размонтировано. Для восстановления контейнера:"
|
||||
echo " pct create NEW_VMID $OUTPUT_FILE --restore 1 --storage local-lvm"
|
||||
13
scripts/telegram-notify.env.example
Normal file
13
scripts/telegram-notify.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Пример конфига для уведомлений в Telegram (скрипт notify-telegram.sh).
|
||||
# Скопируйте на хост Proxmox в /root/.telegram-notify.env и подставьте свои значения.
|
||||
#
|
||||
# Как получить:
|
||||
# 1. Создать бота: в Telegram написать @BotFather, команда /newbot, получить токен.
|
||||
# 2. Узнать chat_id: написать боту любое сообщение, затем открыть в браузере:
|
||||
# https://api.telegram.org/bot<TOKEN>/getUpdates
|
||||
# В ответе в updates[].message.chat.id — ваш chat_id (число или отрицательное для групп).
|
||||
#
|
||||
# На хосте: cp telegram-notify.env.example /root/.telegram-notify.env && chmod 600 /root/.telegram-notify.env
|
||||
|
||||
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
||||
TELEGRAM_CHAT_ID=123456789
|
||||
Reference in New Issue
Block a user