Compare commits
5 Commits
feaa31f702
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 604f0c705f | |||
| 53769e6832 | |||
| 16c254510a | |||
| f319133cee | |||
| 56cee83198 |
@@ -4,7 +4,7 @@
|
||||
|
||||
**Точка входа:** [Архитектура и подключение](docs/architecture/architecture.md) — схема сети, IP, домены, таблица всех хостов.
|
||||
**Топология и риски:** [Схема сети и зависимости](docs/network/network-topology.md) — узлы, маршруты NPM, зависимости сервисов, единые точки отказа (SPOF).
|
||||
**Приоритет №1:** [Бэкапы Proxmox (фаза 1)](docs/backup/proxmox-phase1-backup.md) — стратегия бэкапов LXC/VM и /etc/pve, тестовое восстановление.
|
||||
**Приоритет №1:** [Бэкапы: как устроены и как восстанавливать](docs/backup/backup-howto.md) — что бэкапится, куда, когда и как восстановить.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
- **Внешний IP:** 185.35.193.144
|
||||
- **Домашний сервер (Proxmox):** 192.168.1.150 (LAN)
|
||||
- Подключение: `ssh root@192.168.1.150`
|
||||
- **Прямой SSH на контейнеры и ВМ:** `ssh root@192.168.1.{100,101,103,104,105,107,108,109}`; ВМ 200: `ssh admin@192.168.1.200`. Ключи развёртываются скриптом `scripts/deploy-ssh-keys-homelab.sh`.
|
||||
- **DNS домена katykhin.ru:** Beget.com
|
||||
- Учётная запись: логин `amauri7g`, пароль `QgkaKL3RykeI`, ID аккаунта 2536839. Режим API включён. Домен **katykhin.store** в аккаунте есть, но не используется (поддоменов нет).
|
||||
- Учётная запись: логин и пароль в Vaultwarden (объект **beget**). Режим API включён. Домен **katykhin.store** в аккаунте есть, но не используется (поддоменов нет).
|
||||
- **Reverse proxy и SSL:** Nginx Proxy Manager (NPM) на контейнере 100.
|
||||
|
||||
**Поддомены katykhin.ru:**
|
||||
@@ -23,6 +24,7 @@
|
||||
| cloud.katykhin.ru | — |
|
||||
| docs.katykhin.ru | — |
|
||||
| git.katykhin.ru | — |
|
||||
| healthchecks.katykhin.ru | Healthchecks (Dead man's switch для бэкапов; на VPS Миран) |
|
||||
| home.katykhin.ru | Homepage |
|
||||
| immich.katykhin.ru | — |
|
||||
| mini-lm.katykhin.ru | — |
|
||||
@@ -93,8 +95,9 @@ pct create 105 local:vztmpl/debian-12-standard_12.2-1_amd64.tar.zst \
|
||||
|
||||
## Дополнительно
|
||||
|
||||
- **Хост Proxmox:** скрипты, таймеры, пути — [host-proxmox.md](../containers/host-proxmox.md).
|
||||
- **Схема сети и зависимости:** полная топология (роутер, Proxmox, контейнеры, VPS), таблица IP/доменов, маршруты NPM, кто от кого зависит, единые точки отказа (SPOF). → [Схема сети и зависимости](../network/network-topology.md).
|
||||
- **Homepage:** на контейнере 103, конфиг сервисов в `/opt/docker/homepage/config/services.yaml` (ссылки на NPM, Invidious, AdGuard, Immich, Galene, Vaultwarden и т.д.).
|
||||
- **Homepage:** на контейнере 103, конфиг сервисов в `/opt/docker/homepage/config/services.yaml` (ссылки на NPM, Invidious, AdGuard, Immich, Galene, Vaultwarden, Healthchecks, Netdata и т.д.).
|
||||
- **VPN (VPS):** отдельный сервер 185.103.253.99, AmneziaWG для обхода блокировок. → [VPN-сервер (VPS, AmneziaWG)](../vps/vpn-vps-amneziawg.md).
|
||||
- **Роутер:** Netcraze Speedster, два WireGuard/AmneziaWG (Германия / США), маршрутизация части трафика через VPN. → [Роутер Netcraze Speedster](../network/router-netcraze-speedster.md).
|
||||
- **VPS Миран (СПБ):** боты (telegram-helper-bot, anonBot), prod-инфраструктура, STUN/TURN для Galene. → [VPS Миран: боты и STUN/TURN](../vps/vps-miran-bots.md).
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
|
||||
Все локальные бэкапы лежат на отдельном диске хоста Proxmox: **/dev/sdb1**, смонтирован в **/mnt/backup**.
|
||||
|
||||
### Карта дисков (Proxmox host)
|
||||
|
||||
| Устройство | Тип | Размер | Использование |
|
||||
|--------------|------|--------|----------------------------------|
|
||||
| /dev/nvme0n1 | NVMe | 256 GB | LVM (система, local-lvm) |
|
||||
| /dev/sda | HDD | 2 TB | ZFS |
|
||||
| /dev/sdb | SSD | 2 TB | ext4, /mnt/backup |
|
||||
| /dev/sdc | HDD | 2 TB | ZFS (RAID1 с sda) |
|
||||
| /dev/sdd | HDD | 8 TB | ext4 (внешний) |
|
||||
|
||||
```
|
||||
/mnt/backup/
|
||||
├── proxmox/
|
||||
@@ -25,28 +35,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** (timer: `backup-vps-miran`) | БД: 14 дней; voice_users и S3 — перезапись | 🖥️ VPS Миран |
|
||||
| **БД Nextcloud (PostgreSQL)** | CT 101, контейнер `nextcloud-db-1` в `/opt/nextcloud` | `/mnt/backup/databases/ct101-nextcloud/` | **01:15** (timer: `backup-ct101-pgdump`) | 14 дней | 🗄️ Nextcloud (БД) |
|
||||
| **Оригиналы фото Immich** | VM 200, `/mnt/data/library/` | `/mnt/backup/photos/library/` | **01:30** (timer: `backup-immich-photos`) | Все копии (без автоудаления) | 📷 Фото 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** (timer: `backup-vps-mtproto`) | 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 (timer 03:00) |
|
||||
| **Конфиги хоста** | `/etc/pve`, `/etc/network/interfaces`, `/etc/hosts`, `/etc/resolv.conf` | `/mnt/backup/proxmox/etc-pve/` | **02:15** (timer: `backup-etc-pve`) | 30 дней | ⚙️ Конфиги хоста |
|
||||
| **БД Paperless (PostgreSQL)** | CT 104, контейнер `paperless-db-1` в `/opt/paperless` | `/mnt/backup/databases/ct104-paperless/` | **02:30** (timer: `backup-ct104-pgdump`) | 14 дней | 🗄️ Paperless (БД) |
|
||||
| **Данные Vaultwarden (пароли)** | CT 103, `/opt/docker/vaultwarden/data` | `/mnt/backup/other/vaultwarden/`; каталог в restic → Yandex | **02:45** (timer: `backup-vaultwarden-data`) | 14 дней | 🔐 Vaultwarden |
|
||||
| **БД Gitea (PostgreSQL)** | CT 103, контейнер `gitea-db-1` в `/opt/gitea` | `/mnt/backup/databases/ct103-gitea/` | **03:00** (timer: `backup-ct103-gitea-pgdump`) | 14 дней | 🗄️ Gitea (БД) |
|
||||
| **БД Immich (PostgreSQL)** | VM 200, контейнер `database` в `/opt/immich` | `/mnt/backup/databases/vm200-immich/` | **03:15** (timer: `backup-vm200-pgdump`) | 14 дней | 🗄️ Immich (БД) |
|
||||
| **Векторы RAG (CT 105)** | CT 105, `/home/rag-service/data/vectors/` (vectors.npz) | `/mnt/backup/other/ct105-vectors/` | **03:30** (timer: `backup-ct105-vectors`) | 14 дней | 📐 Векторы RAG |
|
||||
| **Выгрузка в Yandex (restic)** | `/mnt/backup` без photos | Yandex Object Storage | **04:00** (timer: `backup-restic-yandex`) | 3 daily, 2 weekly, 2 monthly | ☁️ Restic Yandex |
|
||||
| **Выгрузка в Yandex (restic, фото)** | `/mnt/backup/photos` | Yandex Object Storage (тот же репо) | **04:10** (timer: `backup-restic-yandex-photos`) | 3 daily, 2 weekly, 2 monthly | 📷 Restic Yandex (photos) |
|
||||
|
||||
**Окно бэкапов:** внутренние копии — **01:00–03:30**; выгрузка в облако — **04:00** (основной restic), **04:10** (restic фото). **04:35** — ping Healthchecks (Dead man's switch). **05:00** зарезервировано под плановую перезагрузку сервера. Задание vzdump — из веб-интерфейса Proxmox (Центр обработки данных → Резервная копия).
|
||||
|
||||
---
|
||||
|
||||
@@ -56,36 +70,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 +130,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 +148,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 +158,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 +168,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 +185,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 +257,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,31 +268,278 @@ 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**. Пароли и дампы — чувствительные данные; не выкладывать в открытый доступ.
|
||||
|
||||
---
|
||||
|
||||
## Скрипты на хосте Proxmox
|
||||
## Восстановление из restic (Yandex)
|
||||
|
||||
| Скрипт | Назначение | 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-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/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 * * * |
|
||||
**Когда нужно:** локальных бэкапов нет (потеря диска, другой хост), данные есть только в 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.
|
||||
|
||||
---
|
||||
|
||||
## Скрипты и systemd timers на хосте Proxmox
|
||||
|
||||
Бэкапы запускаются через **systemd timers** (миграция с cron). Unit-файлы: `scripts/systemd/`. Копировать на хост: `cp scripts/systemd/*.service scripts/systemd/*.timer /etc/systemd/system/`, затем `systemctl daemon-reload` и `systemctl enable --now <timer>`.
|
||||
|
||||
| Скрипт | Timer | Расписание |
|
||||
|--------|-------|------------|
|
||||
| `backup-vps-miran.sh` | backup-vps-miran.timer | 01:00 |
|
||||
| `backup-ct101-pgdump.sh` | backup-ct101-pgdump.timer | 01:15 |
|
||||
| `backup-immich-photos.sh` | backup-immich-photos.timer | 01:30 |
|
||||
| `backup-vps-mtproto.sh` | backup-vps-mtproto.timer | 01:45 |
|
||||
| `backup-etc-pve.sh` | backup-etc-pve.timer | 02:15 |
|
||||
| `backup-ct104-pgdump.sh` | backup-ct104-pgdump.timer | 02:30 |
|
||||
| `backup-vaultwarden-data.sh` | backup-vaultwarden-data.timer | 02:45 |
|
||||
| `backup-ct103-gitea-pgdump.sh` | backup-ct103-gitea-pgdump.timer | 03:00 |
|
||||
| `notify-vzdump-success.sh` | notify-vzdump-success.timer | 03:00 |
|
||||
| `backup-vm200-pgdump.sh` | backup-vm200-pgdump.timer | 03:15 |
|
||||
| `backup-ct105-vectors.sh` | backup-ct105-vectors.timer | 03:30 |
|
||||
| `backup-restic-yandex.sh` | backup-restic-yandex.timer | 04:00 |
|
||||
| `backup-restic-yandex-photos.sh` | backup-restic-yandex-photos.timer | 04:10 |
|
||||
| `healthcheck-ping.sh` | backup-healthcheck-ping.timer | 04:35 (Healthchecks) |
|
||||
| `watchdog-timers.sh` | backup-watchdog-timers.timer | 12:00 (проверка failed timers, .ok) |
|
||||
|
||||
**Healthcheck-файлы:** при успешном завершении каждый скрипт бэкапа пишет `echo $(date -Iseconds) > /var/run/backup-<name>.ok`. Watchdog проверяет раз в день: если файл старше 24 ч — алерт в Telegram.
|
||||
|
||||
**Тест восстановления:** см. [restore-test-manual.md](restore-test-manual.md). Автоматические скрипты: `verify-restore-level1.sh` (restic check, дамп Nextcloud), `verify-vzdump-level2.sh` (vzdump CT 107). Таймеры: `verify-restore-level1-weekly`, `-monthly-check`, `-full-check`, `-monthly-dump`, `verify-vzdump-level2`.
|
||||
|
||||
Задание 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-secrets.md](../vaultwarden-secrets.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
|
||||
```
|
||||
|
||||
**Запуск из systemd timer и доступ к Vaultwarden (bw):** В окружении systemd timer `PATH` может не содержать `/usr/local/bin`, где обычно установлен `bw`. Скрипты дампов БД задают `export PATH="/usr/local/bin:..."`, поэтому `bw` и `jq` находятся. Нужно: 1) файл `/root/.bw-master` (chmod 600) с мастер-паролем; 2) один раз: `bw config server https://vault.katykhin.ru`, `bw login`; 3) при каждом запуске скрипт делает `bw unlock --passwordfile /root/.bw-master --raw`. Если вручную дамп идёт, а из таймера — нет, проверьте `/root/.bw-master` и PATH.
|
||||
|
||||
### Почему размер дампа меньше размера БД на диске
|
||||
|
||||
`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 — таймер `notify-vzdump-success` в **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 и не ломает вызывающие скрипты.
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [Стратегия бэкапов (фаза 1)](proxmox-phase1-backup.md) — общий план и принятые решения.
|
||||
- [Vaultwarden и секреты](../vaultwarden-secrets.md) — получение паролей через `bw` для скриптов бэкапов.
|
||||
- [Архитектура](../architecture/architecture.md) — хост, IP, доступ.
|
||||
- [VM 200 (Immich)](../containers/container-200.md) — сервисы, пути, .env.
|
||||
- [Ручной тест восстановления](restore-test-manual.md) — пошаговые команды для полной проверки restore.
|
||||
- [Healthchecks на VPS Миран](../vps/healthchecks-miran-setup.md) — Dead man's switch, ping после бэкапов.
|
||||
- [Netdata на Proxmox](../monitoring/netdata-proxmox-setup.md) — мониторинг CPU, RAM, дисков, алерты в Telegram.
|
||||
- [SMART и smartd](../monitoring/smartd-setup.md) — мониторинг дисков, уведомления при отклонениях.
|
||||
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
# Фаза 1: Стратегия бэкапов Proxmox
|
||||
|
||||
Цель: при смерти SSD с системой или потере `/etc/pve` — развернуть Proxmox, восстановить контейнеры/ВМ, поднять сервисы без многочасового восстановления и угадывания паролей.
|
||||
|
||||
**Приоритет №1.** ИБП уже есть; защищаемся от смерти диска.
|
||||
|
||||
---
|
||||
|
||||
## Что бэкапить
|
||||
|
||||
| Объект | Зачем |
|
||||
|--------|--------|
|
||||
| **LXC и VM целиком** (vzdump) | Восстановление контейнера/ВМ из одного архива: ОС, конфиги, данные на корневом томе. Не только данные внутри — сам образ для restore. |
|
||||
| **/etc/pve** | Конфиги кластера, VM/LXC (ID, сеть, диски, задачи), пользователи Proxmox, права. Без этого после переустановки Proxmox не восстановить привязку дисков и настройки. |
|
||||
|
||||
---
|
||||
|
||||
## Пошаговый план
|
||||
|
||||
### Шаг 1. Определить хранилище для бэкапов
|
||||
|
||||
**Выбранная схема:**
|
||||
|
||||
| Место | Описание | Что туда |
|
||||
|-------|----------|----------|
|
||||
| **Локально: /dev/sdb1 (1 ТБ под бэкапы)** | Отдельный SSD 2 ТБ; под копии выделен 1 ТБ, смонтирован в `/mnt/backup`. Второй ТБ — в запас. | Proxmox vzdump (через UI), затем те же данные (dump, etc-pve, фотки, VPS) в Yandex через restic. Фотки: оригиналы + метаданные + БД Immich; остальное пересчитать можно. VPS: Amnezia — конфиг; Миран — БД бота + контент (контент можно в S3 Мирана; копию на sdb1 — опционально). Конфигурацию серверов не бэкапим — есть Ansible. |
|
||||
| **Офсайт: Yandex Object Storage (S3)** | Арендованный бакет, S3-совместимый API. [Yandex Object Storage](https://yandex.cloud/ru/docs/storage/s3/). | **Restic** с хоста (cron на ноде Proxmox): выгрузка содержимого `/mnt/backup`. Retention: 3 daily, 2 weekly, 2 monthly. |
|
||||
|
||||
**Принято:** Вариант A — отдельный диск/раздел на хосте (sdb1, 1 ТБ в `/mnt/backup`). Варианты B (NFS/SMB) и C (внешний USB) в текущей схеме не используются; USB опционально для параноидального 3-2-1 (см. выше).
|
||||
|
||||
**3-2-1:** Три копии: (1) прод — система и данные на основном диске; (2) локальный бэкап — sdb1; (3) офсайт — Yandex. Два типа носителей: локальный SSD и облачное object storage. Один офсайт — да. **Стратегия удовлетворяет 3-2-1.** Опционально: четвёртая копия на внешнем USB/другом ПК для параноидального сценария (пожар/кража) — по желанию.
|
||||
|
||||
**Принято:** Точка монтирования — `/mnt/backup`. На диске 2 ТБ выделено 1 ТБ под бэкапы; второй ТБ пока в запас, назначение не определено.
|
||||
|
||||
**Действие:** Разметить 1 ТБ на /dev/sdb1, создать ФС (ext4/xfs), смонтировать в `/mnt/backup`. Структуру каталогов — см. раздел «Структура локального хранилища» ниже.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 2. Добавить Backup Storage в Proxmox
|
||||
|
||||
1. В веб-интерфейсе: **Datacenter → Storage → Add**.
|
||||
2. Тип: **Directory** (если папка на локальном диске) или **NFS/CIFS** (если сетевое).
|
||||
3. Указать:
|
||||
- **ID** (например `backup-local` или `backup-nfs`),
|
||||
- **Directory** — путь, например `/mnt/backup/dump`,
|
||||
- Включить опции **Content: VZDump backup file** (и при необходимости **ISO**, **Container template** — по желанию).
|
||||
4. Сохранить. Убедиться, что storage виден и доступен для записи.
|
||||
|
||||
Если используешь NFS: сначала смонтировать NFS в `/mnt/backup` на хосте (fstab или systemd mount), затем добавить Storage с Directory `/mnt/backup/dump`.
|
||||
|
||||
**Для выбранной схемы:** Directory = `/mnt/backup/proxmox/dump` (см. структуру ниже).
|
||||
|
||||
---
|
||||
|
||||
### Структура локального хранилища (1 ТБ на sdb1)
|
||||
|
||||
«Аналог S3» на одном диске — это просто понятная иерархия каталогов. **MinIO не используем:** лишний сервис; достаточно директорий и restic с бэкендом `local` (если понадобится локальный restic) и `s3` для Yandex. Proxmox пишет в **Directory** — ему достаточно пути.
|
||||
|
||||
Пример структуры под `/mnt/backup`:
|
||||
|
||||
```
|
||||
/mnt/backup/
|
||||
├── proxmox/
|
||||
│ ├── dump/ ← Proxmox Backup Job (VZDump) — сюда добавляем Storage в PVE
|
||||
│ └── etc-pve/ ← архивы tar.gz из cron: etc-pve-*, etc-host-configs-* (backup-etc-pve.sh)
|
||||
├── restic/
|
||||
│ ├── local/ ← репозитории restic для локальных снапшотов (опционально)
|
||||
│ └── ... ← или restic только в Yandex, локально только сырые копии
|
||||
├── photos/ ← Immich: оригиналы фото + метаданные + БД (остальное пересчитать)
|
||||
├── vps/ ← Amnezia: конфиг. Миран: БД бота (+ контент при необходимости; основной контент можно в S3 Мирана)
|
||||
└── other/ ← прочие важные данные (конфиги, скрипты, что ещё решите)
|
||||
```
|
||||
|
||||
Квоты: при необходимости ограничить размер по каталогам (например `proxmox/dump` — не более 500 ГБ) через отдельные подразделы или скрипты очистки (retention).
|
||||
|
||||
---
|
||||
|
||||
### Шифрование диска бэкапов (LUKS)
|
||||
|
||||
**LUKS** (Linux Unified Key Setup) — стандартное шифрование раздела в Linux. Если диск с бэкапами украдут или вынесут, без пароля/ключа данные не прочитать. Минусы: нужно вводить пароль при загрузке (или хранить ключ на другом носителе), небольшая нагрузка на CPU.
|
||||
|
||||
**Принято:** LUKS пока не используем. Раздел sdb1 — без шифрования. При необходимости можно добавить позже (потребуется перенос данных).
|
||||
|
||||
---
|
||||
|
||||
### Restic и Yandex Object Storage
|
||||
|
||||
- **Restic** поддерживает бэкенд **S3**. Yandex Object Storage совместим с S3 API — используешь endpoint бакета и ключи (Access Key / Secret).
|
||||
- **Retention в Yandex:** 3 daily, 2 weekly, 2 monthly — `restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2` и затем `prune`.
|
||||
- **Что гнать в Yandex через restic:** Proxmox dump (каталог `proxmox/dump`), `/etc/pve` (архивы из `proxmox/etc-pve`), фотки (оригиналы + метаданные + БД Immich), бэкапы с VPS — см. «Принятые решения: что куда» ниже.
|
||||
|
||||
**Локально отдельной политики retention для restic не нужно:** на sdb1 retention задаётся в самом Proxmox Backup Job (например «keep last 7») и в скрипте бэкапа `/etc/pve` (удалять архивы старше N дней). Restic используется только для отправки в Yandex с политикой 7/4/6. Локальных restic-репозиториев можно не заводить — только каталоги и выгрузка их содержимого в облако.
|
||||
|
||||
Документация Yandex: [Object Storage S3](https://yandex.cloud/ru/docs/storage/s3/). Нужны: bucket name, region, endpoint, Static Key (Access Key ID + Secret Access Key). **Бакет создан; ключи и endpoint зафиксировать при настройке restic.**
|
||||
|
||||
---
|
||||
|
||||
### Хранилище паролей
|
||||
|
||||
Чтобы не терять пароли при восстановлении и держать креды в одном месте. Рассматривались варианты: Vaultwarden (self-hosted), Bitwarden Cloud, KeePass/KeePassXC, 1Password и др.
|
||||
|
||||
**Принято и сделано:** **Vaultwarden** развёрнут на **CT 103** LXC. Домен через NPM (HTTPS), клиенты Bitwarden на ПК/телефоне. Бэкап данных Vaultwarden включён в общий план (restic → Yandex).
|
||||
|
||||
---
|
||||
|
||||
### Где запускать бэкапы (централизация)
|
||||
|
||||
**С хоста Proxmox (cron на ноде):**
|
||||
|
||||
- **Proxmox Backup Job** уже выполняется на хосте и пишет vzdump всех выбранных LXC/VM в `/mnt/backup/proxmox/dump` — это и есть «бэкапы всех контейнеров и ВМ», централизованно.
|
||||
- **Restic** (backup/forget/prune) тоже запускаем с хоста по cron: бэкапит каталоги на хосте (например весь `/mnt/backup` или выбранные подкаталоги) в Yandex S3. Данные для бэкапа — локальные пути (dump, etc-pve, а фотки и данные с VPS нужно либо копировать на хост в `/mnt/backup` скриптами, либо монтировать и тогда restic будет их читать с хоста). Контейнеры и ВМ целиком не бэкапим через restic — ими занимается только Proxmox Backup Job.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 3. Настроить расписание бэкапа LXC и VM
|
||||
|
||||
1. **Datacenter → Backup** (или **Backup** в меню узла).
|
||||
2. **Add** — создаётся задача (Job).
|
||||
3. Параметры:
|
||||
- **Storage** — выбранное хранилище (шаг 2).
|
||||
- **Schedule** — например `0 2 * * *` (каждую ночь в 02:00). Подстроить под окно, когда нагрузка минимальна.
|
||||
- **Selection mode:** включить нужные узлы (или **All**), затем отметить **галочками** конкретные **LXC (100–108) и VM (200)**. Либо выбрать "Backup all" для всех VMs/containers.
|
||||
- **Mode:**
|
||||
- **Snapshot** — контейнер/ВМ не останавливается, создаётся снимок (рекомендуется для минимизации даунтайма).
|
||||
- **Suspend** — ВМ приостанавливается на время бэкапа (более консистентно для БД, но даунтайм).
|
||||
Для LXC обычно достаточно **Snapshot**. Для **VM 200** (PostgreSQL и др.): Snapshot **не гарантирует консистентность БД** — PostgreSQL может быть в середине транзакции. **Правильная стратегия:** внутри VM делать логический бэкап БД (`pg_dump`), а **vzdump snapshot** использовать для остального (ОС, конфиги, файлы). Итого: VM 200 — vzdump snapshot ок для образа; консистентность БД — отдельно через `pg_dump` внутри гостя.
|
||||
- **Compression:** ZSTD (хороший компромисс скорость/размер).
|
||||
- **Retention:** например «Keep last 7» или «Keep last 4 weekly» — чтобы не забивать диск.
|
||||
|
||||
4. Сохранить job. Проверить по кнопке **Backup now**, что задача запускается и файлы появляются в Storage.
|
||||
|
||||
Важно: бэкап должен включать **и LXC, и VM 200**. Не только данные внутри них (те уже описаны в документации контейнеров), а именно полный dump для restore.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 4. Бэкап /etc/pve и конфигов хоста
|
||||
|
||||
Конфиги кластера и виртуалок лежат в `/etc/pve`. Плюс для восстановления хоста полезны: `/etc/network/interfaces`, `/etc/hosts`, `/etc/resolv.conf`. Всё это нужно копировать регулярно и хранить в безопасном месте (желательно не только на том же диске, что и система).
|
||||
|
||||
**Принято: вариант A — cron на хосте Proxmox.**
|
||||
|
||||
1. Создать скрипт, например `/root/scripts/backup-etc-pve.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
BACKUP_ROOT="/mnt/backup/proxmox/etc-pve" # по структуре выше
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
mkdir -p "$BACKUP_ROOT"
|
||||
tar -czf "$BACKUP_ROOT/etc-pve-$DATE.tar.gz" -C / etc/pve
|
||||
tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interfaces etc/hosts etc/resolv.conf
|
||||
# опционально: удалять бэкапы старше N дней
|
||||
# find "$BACKUP_ROOT" -name 'etc-pve-*.tar.gz' -mtime +30 -delete
|
||||
# find "$BACKUP_ROOT" -name 'etc-host-configs-*.tar.gz' -mtime +30 -delete
|
||||
```
|
||||
|
||||
2. Сделать исполняемым: `chmod +x /root/scripts/backup-etc-pve.sh`.
|
||||
3. Добавить в cron: `crontab -e`. Окно внутренних бэкапов 01:00–03:30; пример для etc-pve: `15 2 * * * /root/scripts/backup-etc-pve.sh`
|
||||
|
||||
**Вариант B:** Тот же скрипт можно вызывать из задачи в Proxmox (Script/Command в задаче типа Hook script), но проще и надёжнее — отдельный cron на хосте.
|
||||
|
||||
Бэкапы (`etc-pve-*.tar.gz`, `etc-host-configs-*.tar.gz`) хранить **локально** (`/mnt/backup/proxmox/etc-pve`) и **в Yandex** — включить этот каталог в источники restic (мало весит, критично при потере хоста). Файлы с ограниченными правами (chmod 600); `/etc/pve` содержит секреты — не выкладывать в открытый доступ.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 5. Хранить секреты отдельно (пароли, ключи)
|
||||
|
||||
Чтобы «не вспоминать пароли 3 часа» после восстановления:
|
||||
|
||||
**Секретное хранилище** — Vaultwarden на **CT 103** (см. выше). Туда: root Proxmox, пользователи PVE, пароли БД и сервисов (Nextcloud, Gitea, Paperless, Immich, NPM, Galene и т.д.), API-ключи (Beget, certbot, Wallos и др.). Полный список кредов по контейнерам — в статьях container-100 … container-200; свести в один список в Vaultwarden и обновлять при смене паролей.
|
||||
|
||||
Это не «шаг бэкапа», но обязательная часть восстановления: без паролей восстановленные контейнеры не войдут в сервисы.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 6. Тестовое восстановление одного контейнера
|
||||
|
||||
Без проверки восстановления нельзя считать стратегию рабочей.
|
||||
|
||||
1. Выбрать **некритичный** контейнер (например 105 — RAG, или 107 — Invidious), для которого краткий даунтайм допустим.
|
||||
2. Убедиться, что есть свежий backup этого контейнера в Storage (после шага 3).
|
||||
3. **Восстановление:**
|
||||
- В Proxmox UI: **Datacenter → Backup** → выбрать storage → найти backup нужного CT → **Restore**. Указать **new VMID** (например 999 для теста) и **Storage** для дисков.
|
||||
- Или с CLI:
|
||||
`qmrestore` для VM, для LXC — через GUI или `pct restore` (см. справку `pct restore`).
|
||||
Для LXC типично: Backup → Restore → задать новый ID (например 999), node, storage.
|
||||
4. Запустить восстановленный контейнер (ID 999), проверить:
|
||||
- заходит по SSH/консоль;
|
||||
- сервисы внутри запускаются (docker/ systemd);
|
||||
- с хоста пинг и при необходимости один сервис по порту.
|
||||
5. После проверки удалить тестовый контейнер (999), освободить место.
|
||||
|
||||
Если что-то пошло не так (не находится диск, ошибка прав, сеть) — зафиксировать и поправить стратегию (пути storage, режим backup, права).
|
||||
|
||||
---
|
||||
|
||||
### Шаг 7. Документировать процедуру восстановления «с нуля»
|
||||
|
||||
Кратко зафиксировать в отдельном разделе (здесь или в architecture):
|
||||
|
||||
1. Установка Proxmox на новое железо (или новый диск).
|
||||
2. Восстановление конфигов: распаковать последний `etc-pve-*.tar.gz` в `/etc/pve` (с учётом того, что нужно корректно подставить ноды и storage; при одномузловой установке обычно достаточно скопировать файлы).
|
||||
3. Подключение storage с backup (или копирование последних vzdump на новый storage).
|
||||
4. Восстановление контейнеров и ВМ из backup по одному (Restore с указанием VMID и storage).
|
||||
5. Запуск контейнеров/ВМ, проверка сети и сервисов.
|
||||
6. Использование сохранённых паролей/ключей для входа и проверки сервисов.
|
||||
|
||||
После первого успешного тестового восстановления (шаг 6) эту процедуру можно уточнить и дописать по факту.
|
||||
|
||||
---
|
||||
|
||||
## Чек-лист фазы 1
|
||||
|
||||
- [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] Vaultwarden развёрнут (CT 103).
|
||||
- [ ] Секреты перенесены в Vaultwarden. *(на усмотрение: root PVE, пароли БД, API и т.д.)*
|
||||
- [ ] Бэкап данных Vaultwarden включён в restic (Yandex S3). *Локально данные уже копируются в `/mnt/backup/other/vaultwarden/` (backup-vaultwarden-data.sh); при настройке restic — включить этот каталог в источники.*
|
||||
- [ ] Выполнено тестовое восстановление одного контейнера (другой VMID), проверена работоспособность.
|
||||
- [x] В документации зафиксирована процедура полного восстановления Proxmox «с нуля». *[backup-howto.md](backup-howto.md): восстановление из vzdump, конфигов, БД, VM 200 с нуля, Vaultwarden, VPS и др.*
|
||||
|
||||
---
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [Архитектура и подключение](../architecture/architecture.md) — хосты, IP, домены.
|
||||
- [Схема сети и зависимости](../network/network-topology.md) — SPOF, зависимость от Proxmox и бэкапов.
|
||||
- Документация контейнеров (100–108, 200) — бэкапы *данных внутри* сервисов (БД, тома); фаза 1 дополняет это бэкапом на уровне PVE.
|
||||
|
||||
---
|
||||
|
||||
## Принятые решения (сводка)
|
||||
|
||||
| Вопрос | Решение |
|
||||
|--------|---------|
|
||||
| Точка монтирования, второй ТБ | `/mnt/backup`; второй ТБ на sdb1 — в запас, назначение позже. |
|
||||
| Шифрование (LUKS) | Пока не делаем; раздел без шифрования. |
|
||||
| Proxmox vzdump | Локально в `proxmox/dump` + дублировать в Yandex через restic. |
|
||||
| Фотки | Оригиналы + метаданные + БД Immich; остальное пересчитать. |
|
||||
| VPS | Amnezia — конфиг. Миран — БД бота + контент (контент можно в S3 Мирана; копия на sdb1 — по желанию). Конфиг серверов не бэкапим — Ansible. |
|
||||
| Где запускать бэкапы | Cron на хосте Proxmox: Backup Job (vzdump) + restic в Yandex. |
|
||||
| Retention локально | Только в Proxmox Job и в скрипте etc-pve; отдельного restic-репозитория локально не делаем. |
|
||||
| /etc/pve + конфиги хоста (interfaces, hosts, resolv.conf) | Вариант A: cron на хосте → `etc-pve` и `etc-host-configs` в `/mnt/backup/proxmox/etc-pve`; локально и в Yandex (restic). |
|
||||
| Пароли | Vaultwarden на CT 103. |
|
||||
| VM 200 (БД PostgreSQL) | vzdump snapshot — для образа ВМ; консистентность БД — отдельно: внутри VM логический бэкап (`pg_dump`). |
|
||||
| Yandex | Бакет создан; ключи и endpoint зафиксировать при настройке restic. |
|
||||
| MinIO | Не используем; директории + restic (s3 для Yandex). |
|
||||
|
||||
---
|
||||
|
||||
## Осталось сделать
|
||||
|
||||
- **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 и обновлять при смене.
|
||||
140
docs/backup/restore-test-manual.md
Normal file
140
docs/backup/restore-test-manual.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Ручной тест восстановления (уровень 3)
|
||||
|
||||
Пошаговые команды для полной проверки восстановления после потери данных или миграции. Выполнять периодически (раз в 6–12 месяцев) или после значительных изменений инфраструктуры.
|
||||
|
||||
---
|
||||
|
||||
## 1. Полный restore на отдельный диск
|
||||
|
||||
**Когда нужно:** проверка, что все бэкапы доступны и можно восстановить систему на новом диске.
|
||||
|
||||
### Подготовка
|
||||
|
||||
1. Подключить диск с достаточным объёмом (например 2 TB) или использовать временный раздел.
|
||||
2. Смонтировать в `/mnt/restore-test` (или аналогичный путь).
|
||||
3. Убедиться, что есть креды restic: `/root/.restic-yandex.env`, `/root/.restic-password` или Vaultwarden (объект RESTIC).
|
||||
|
||||
### Восстановление из restic (Yandex)
|
||||
|
||||
```bash
|
||||
# Список снимков
|
||||
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
|
||||
|
||||
# Восстановить основной снимок (без photos) в каталог
|
||||
restic restore latest --target /mnt/restore-test --path /mnt/backup
|
||||
|
||||
# Восстановить фото (отдельный снимок)
|
||||
restic snapshots | grep photos
|
||||
restic restore <SNAPSHOT_ID> --target /mnt/restore-test --path /mnt/backup/photos
|
||||
```
|
||||
|
||||
Файлы появятся в `/mnt/restore-test/mnt/backup/`. Проверить наличие:
|
||||
- `proxmox/dump/dump/` — vzdump
|
||||
- `proxmox/etc-pve/` — конфиги хоста
|
||||
- `databases/` — дампы БД
|
||||
- `other/vaultwarden/` — архив Vaultwarden
|
||||
- `photos/library/` — фото Immich
|
||||
|
||||
---
|
||||
|
||||
## 2. Проверка Immich (веб, загрузка фото)
|
||||
|
||||
**Цель:** убедиться, что Immich работает, веб-интерфейс доступен, загрузка фото проходит.
|
||||
|
||||
### Подготовка
|
||||
|
||||
- Immich доступен по https://immich.katykhin.ru (через NPM).
|
||||
- ВМ 200: `ssh admin@192.168.1.200`.
|
||||
|
||||
### Шаги
|
||||
|
||||
1. **Открыть:** https://immich.katykhin.ru
|
||||
2. **Войти** под учётной записью (логин/пароль из менеджера паролей).
|
||||
3. **Загрузить тестовое фото:**
|
||||
- Нажать «Upload» (или загрузить через drag-and-drop).
|
||||
- Выбрать небольшое изображение (например 1–2 MB).
|
||||
- Дождаться завершения загрузки и появления в библиотеке.
|
||||
4. **Проверить:** фото появилось в галерее, превью отображается, метаданные доступны.
|
||||
|
||||
### Если Immich не загружается
|
||||
|
||||
- Проверить: `ssh admin@192.168.1.200 "cd /opt/immich && docker compose ps"` — все контейнеры running.
|
||||
- Логи: `docker logs immich_server` (или `immich_upload_optimizer`).
|
||||
- NPM: прокси на 192.168.1.200:2283.
|
||||
|
||||
---
|
||||
|
||||
## 3. Проверка Nextcloud (веб, загрузка файла)
|
||||
|
||||
**Цель:** убедиться, что Nextcloud доступен и загрузка файлов работает.
|
||||
|
||||
### Подготовка
|
||||
|
||||
- Nextcloud: https://cloud.katykhin.ru
|
||||
- Контейнер 101: `ssh root@192.168.1.101` или `pct exec 101 -- bash`.
|
||||
|
||||
### Шаги
|
||||
|
||||
1. **Открыть:** https://cloud.katykhin.ru
|
||||
2. **Войти** под учётной записью (логин/пароль из менеджера паролей).
|
||||
3. **Загрузить тестовый файл:**
|
||||
- Перейти в «Files» (или «Файлы»).
|
||||
- Нажать «Upload» или перетащить файл (например .txt или .pdf).
|
||||
- Дождаться завершения загрузки.
|
||||
4. **Проверить:** файл отображается в списке, можно скачать.
|
||||
|
||||
### Если Nextcloud не работает
|
||||
|
||||
- Проверить: `pct exec 101 -- docker ps` — контейнеры nextcloud и nextcloud-db-1 running.
|
||||
- Логи: `docker logs nextcloud-app-1` (или имя контейнера из compose).
|
||||
|
||||
---
|
||||
|
||||
## 4. Проверка GPU passthrough на VM 200
|
||||
|
||||
**Цель:** убедиться, что GPU проброшена в Immich ML и распознавание работает.
|
||||
|
||||
### Подготовка
|
||||
|
||||
- VM 200: `ssh admin@192.168.1.200`
|
||||
- В Immich: включить ML (Settings → Machine Learning).
|
||||
|
||||
### Шаги
|
||||
|
||||
1. **Проверить GPU в контейнере ML:**
|
||||
```bash
|
||||
ssh admin@192.168.1.200
|
||||
cd /opt/immich
|
||||
docker exec immich_machine_learning nvidia-smi
|
||||
```
|
||||
Ожидаемый вывод: информация о GPU (модель, память, драйвер).
|
||||
|
||||
2. **Проверить распознавание в Immich:**
|
||||
- Загрузить фото с лицами или объектами.
|
||||
- Дождаться обработки ML (иконка «Scan» в интерфейсе).
|
||||
- Проверить: объекты/лица распознаны, теги добавлены.
|
||||
|
||||
3. **Если nvidia-smi не работает:**
|
||||
- На хосте Proxmox: проверить `hostpci0` в конфиге VM 200: `cat /etc/pve/qemu-server/200.conf`
|
||||
- Убедиться, что PCI-устройство GPU передано в ВМ (`hostpci0: 0000:xx:00.0` и т.п.).
|
||||
- Перезапустить ВМ при необходимости: `qm stop 200 && qm start 200`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Дополнительные проверки (по желанию)
|
||||
|
||||
- **Vaultwarden:** https://vault.katykhin.ru — вход, синхронизация.
|
||||
- **Gitea:** https://git.katykhin.ru — вход, список репозиториев.
|
||||
- **Paperless:** https://docs.katykhin.ru — вход, поиск документов.
|
||||
- **Galene:** https://call.katykhin.ru — вход в комнату.
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [backup-howto](backup-howto.md) — восстановление из vzdump, restic, дампов БД, расписание таймеров.
|
||||
- [container-200](../containers/container-200.md) — VM 200 (Immich), GPU, пути.
|
||||
- [architecture](../architecture/architecture.md) — хост, IP, доступ.
|
||||
@@ -18,9 +18,9 @@
|
||||
|
||||
## Доступ и логины
|
||||
|
||||
- **Debian (CT 100):** логин `root` (или консольный пользователь Debian), пароль `waccEk-fyqbux-rarja3`.
|
||||
- **AdGuard Home (через домен):** https://adguard.katykhin.ru (через NPM, сертификат Let's Encrypt), пользователь `kerrad`, пароль `waccEk-fyqbux-rarja3`. Прямой доступ по порту 3000 больше не используется.
|
||||
- **Nginx Proxy Manager:** http://192.168.1.100:81, имя `Kerrad`, email `j3tears100@gmail.com`, пароль `kqEUubVq02DJTS8`.
|
||||
- **Debian (CT 100):** логин `root`. Пароль — в Vaultwarden (объект **CT_100_ROOT_PASSWORD**).
|
||||
- **AdGuard Home (через домен):** https://adguard.katykhin.ru (через NPM, сертификат Let's Encrypt). Пользователь и пароль — в Vaultwarden (объект **ADGUARD**). Прямой доступ по порту 3000 больше не используется.
|
||||
- **Nginx Proxy Manager:** http://192.168.1.100:81. Имя, email и пароль — в Vaultwarden (объект **NPM_ADMIN**).
|
||||
|
||||
---
|
||||
|
||||
@@ -56,8 +56,10 @@
|
||||
|
||||
**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`.
|
||||
- Учётные данные Beget API: `/root/.secrets/certbot/beget.ini` (генерируется из Vaultwarden скриптом `deploy-beget-credentials.sh` с хоста Proxmox).
|
||||
- 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).
|
||||
|
||||
@@ -179,14 +181,23 @@ docker restart wallos
|
||||
|
||||
Проверяет, идут ли запросы к заданным доменам через VPN или через основное подключение (подключение к роутеру по telnet, разбор маршрутов). Результаты отдаёт на порту **8765** (на хосте). В Homepage добавлена ссылка на http://192.168.1.100:8765.
|
||||
|
||||
**Переменные окружения в compose:** `ROUTER_TELNET_HOST`, `ROUTER_TELNET_USER`, `ROUTER_TELNET_PASSWORD` — **заданы в самом файле** (не в .env). Рекомендация: вынести в `.env` и не коммитить пароль (см. TODO).
|
||||
**Секреты:** `ROUTER_TELNET_HOST`, `ROUTER_TELNET_USER`, `ROUTER_TELNET_PASSWORD` берутся из Vaultwarden (объект **localhost**). Деплой — единым скриптом на Proxmox:
|
||||
|
||||
```bash
|
||||
/root/scripts/deploy-vpn-route-check.sh
|
||||
```
|
||||
|
||||
Скрипт: разблокирует bw, получает креды из Vaultwarden, атомарно пишет `.env` в CT 100, запускает `docker compose up -d`. Режим проверки без записи: `--dry-run`. Шаблон compose: `scripts/vpn-route-check/docker-compose.yml`.
|
||||
|
||||
**Том:** volume `vpn-route-check-data` → `/data` (в контейнере).
|
||||
|
||||
**Команды:**
|
||||
```bash
|
||||
cd /opt/docker/vpn-route-check && docker compose up -d
|
||||
docker logs vpn-route-check
|
||||
# Деплой (с хоста Proxmox)
|
||||
/root/scripts/deploy-vpn-route-check.sh
|
||||
|
||||
# Логи
|
||||
pct exec 100 -- docker logs vpn-route-check
|
||||
```
|
||||
|
||||
---
|
||||
@@ -222,7 +233,7 @@ docker logs vpn-route-check
|
||||
1. Создать сеть (если ещё нет): `docker network create proxy_network`.
|
||||
2. NPM: `cd /opt/docker/nginx-proxy && docker compose up -d`.
|
||||
3. AdGuard: `cd /opt/docker/adguard && docker compose up -d` (создаёт свою сеть и подключается к proxy_network).
|
||||
4. VPN Route Check: `cd /opt/docker/vpn-route-check && docker compose up -d`.
|
||||
4. VPN Route Check: `/root/scripts/deploy-vpn-route-check.sh` (с хоста Proxmox).
|
||||
5. Log-dashboard: при необходимости запустить контейнер с монтом html и портом 8088.
|
||||
|
||||
После изменений в NPM (proxy, SSL): перезагрузка nginx внутри контейнера — `docker exec npm nginx -s reload`. Certbot продлевает сертификаты по таймеру; deploy-hook’и копируют их в NPM и перезагружают nginx.
|
||||
@@ -232,7 +243,7 @@ docker logs vpn-route-check
|
||||
## Уязвимости и риски
|
||||
|
||||
1. **Пароли и креды в конфигах:** В `services.yaml` (Homepage) хранятся пароли виджетов (AdGuard, NPM, Proxmox). Файл лежит только на сервере; не помещать в публичный репозиторий.
|
||||
2. **VPN Route Check:** Логин и пароль роутера прописаны в `docker-compose.yml`. Доступ к compose = доступ к роутеру. Рекомендуется вынести в `.env` и ограничить права на файл.
|
||||
2. **VPN Route Check:** Креды роутера в `.env` (генерируется из Vaultwarden скриптом `deploy-vpn-route-check.sh`). Файл `.env` не коммитить.
|
||||
3. **AdGuard на 3000:** Веб-интерфейс доступен по порту 3000 на хосте. Доступ из LAN; при необходимости закрыть фаерволом снаружи или использовать только через NPM (proxy).
|
||||
4. **NPM на 81:** Админка NPM по порту 81. Убедиться, что с интернета доступ только через VPN или не пробрасывать 81 наружу.
|
||||
5. **Логи NPM:** Часть логов (fallback_*) не ротируется — возможен рост и заполнение диска (см. TODO).
|
||||
@@ -243,7 +254,7 @@ docker logs vpn-route-check
|
||||
|
||||
- [x] **Логи NPM:** Добавить в logrotate ротацию для `fallback_http_access.log`, `fallback_http_error.log` (и при необходимости других fallback_*) по размеру или по дням — настроено в `npm-nginx.conf` (30 дней / ~512 MB).
|
||||
- [x] **Логи AdGuard:** Ограничить хранение логов запросов/статистики — настроено в `AdGuardHome.yaml` (`querylog.interval = 336h`, `statistics.interval = 336h` ≈ 14 дней).
|
||||
- [ ] **VPN Route Check:** Вынести `ROUTER_TELNET_*` в `.env`, подключать в compose через `env_file`, не коммитить .env в репозиторий.
|
||||
- [x] **VPN Route Check:** Секреты из Vaultwarden (объект localhost), деплой через `deploy-vpn-route-check.sh`.
|
||||
- [ ] **Log-dashboard:** Зафиксировать способ запуска контейнера (отдельный compose или скрипт) и добавить его в документацию/автозапуск при перезагрузке CT.
|
||||
- [ ] **Мониторинг диска:** Настроить оповещение (например, из Prometheus/Alertmanager или скрипт по крону) при заполнении корня или `/opt/docker` выше порога (например 80%).
|
||||
- [ ] **Резервное копирование:** Регулярный бэкап критичных папок (оценка размеров на момент документации):
|
||||
|
||||
@@ -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) в скриптах.
|
||||
|
||||
@@ -43,20 +43,22 @@
|
||||
**Порты:** 3000 (хост) → 3000 (контейнер). NPM (контейнер 100) проксирует https://video.katykhin.ru → 192.168.1.107:3000.
|
||||
|
||||
**Тома и конфиги:**
|
||||
- Invidious не использует отдельные bind‑тома для конфигов/данных — данные хранятся в PostgreSQL (`invidious_postgresdata`), а конфиг задаётся через переменную `INVIDIOUS_CONFIG` в compose (inline YAML).
|
||||
- Invidious не использует отдельные bind‑тома для конфигов/данных — данные хранятся в PostgreSQL (`invidious_postgresdata`), а конфиг задаётся через переменную `INVIDIOUS_CONFIG` в compose.
|
||||
- Отдельных каталогов с логами Invidious на хосте нет — логи идут в stdout контейнера (см. раздел «Логи и ротация»).
|
||||
|
||||
**Основная конфигурация (в docker-compose.yml, секция `environment / INVIDIOUS_CONFIG`):**
|
||||
- `db`: dbname=invidious, user=kemal, password=kemal, host=invidious-db, port=5432, check_tables=true.
|
||||
- `invidious_companion`: URL сервиса companion (`http://companion:8282/companion`).
|
||||
- `invidious_companion_key` и `SERVER_SECRET_KEY` (в companion) — общий секрет между Invidious и Companion (сейчас заданы прямо в compose; **не выкладывать в публичный репозиторий**).
|
||||
- `external_port: 443`, `domain: "video.katykhin.ru"`, `https_only: true` — Invidious знает про внешний домен и порт, отдаёт ссылки на https.
|
||||
- Прочие опции (feeds, captions, hmac_key, default_user_preferences и т.д.).
|
||||
**Секреты:** `POSTGRES_USER`, `POSTGRES_PASSWORD`, `INVIDIOUS_COMPANION_KEY`, `HMAC_KEY` берутся из Vaultwarden (объект **INVIDIOUS**). Деплой с хоста Proxmox:
|
||||
```bash
|
||||
/root/scripts/deploy-invidious-credentials.sh
|
||||
```
|
||||
Скрипт генерирует `.env` из Vaultwarden, атомарно пушит в CT 107, запускает `docker compose up -d --force-recreate`. **Ротация:** сменил пароль/ключи в Vaultwarden → запустил скрипт.
|
||||
|
||||
**Команды:**
|
||||
```bash
|
||||
cd /opt/invidious && docker compose up -d
|
||||
docker logs invidious-invidious-1
|
||||
# Деплой (с хоста Proxmox)
|
||||
/root/scripts/deploy-invidious-credentials.sh
|
||||
|
||||
# Логи
|
||||
pct exec 107 -- docker logs invidious-invidious-1
|
||||
curl -s http://127.0.0.1:3000/api/v1/stats
|
||||
```
|
||||
|
||||
@@ -71,7 +73,7 @@ curl -s http://127.0.0.1:3000/api/v1/stats
|
||||
- volume `companioncache` → `/var/tmp/youtubei.js` (кэш js‑ресурсов YouTube / youtubei).
|
||||
|
||||
**Безопасность:**
|
||||
- `SERVER_SECRET_KEY` совпадает с `invidious_companion_key` в конфиге Invidious — это shared secret для обмена.
|
||||
- `SERVER_SECRET_KEY` совпадает с `invidious_companion_key` — оба берутся из `.env` (генерируется из Vaultwarden).
|
||||
- Контейнер запущен с `read_only: true`, `cap_drop: [ALL]`, `no-new-privileges:true` — хорошая практика sandboxing.
|
||||
|
||||
**Команды:**
|
||||
@@ -89,7 +91,7 @@ docker logs invidious-companion-1
|
||||
- `/opt/invidious/config/sql` → `/config/sql` — SQL‑скрипты инициализации/миграций из репозитория Invidious (~40 KB).
|
||||
- `/opt/invidious/docker/init-invidious-db.sh` → `/docker-entrypoint-initdb.d/init-invidious-db.sh` — скрипт инициализации БД при первом запуске.
|
||||
|
||||
**Переменные окружения:** POSTGRES_DB=invidious, POSTGRES_USER=kemal, POSTGRES_PASSWORD=kemal (заданы в compose; не публиковать).
|
||||
**Переменные окружения:** из `.env` (генерируется `deploy-invidious-credentials.sh` из Vaultwarden).
|
||||
|
||||
**Команды:**
|
||||
```bash
|
||||
@@ -124,16 +126,10 @@ Companion и PostgreSQL доступны только внутри docker-сет
|
||||
|
||||
## Запуск и порядок поднятия
|
||||
|
||||
1. Зайти в каталог: `cd /opt/invidious`.
|
||||
2. Проверить/при необходимости подредактировать `docker-compose.yml` (секция `INVIDIOUS_CONFIG`, домен video.katykhin.ru, секреты).
|
||||
3. Запуск/перезапуск:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
Порядок: сначала поднимается `invidious-db`, затем `invidious` (depends_on с healthcheck), параллельно Companion.
|
||||
1. С хоста Proxmox: `/root/scripts/deploy-invidious-credentials.sh` (генерирует `.env` из Vaultwarden, пушит в CT 107, запускает compose).
|
||||
2. Порядок: `invidious-db` → `invidious` (depends_on с healthcheck), параллельно Companion.
|
||||
|
||||
После изменения конфигурации (секция `INVIDIOUS_CONFIG` или окружения Companion/DB):
|
||||
`cd /opt/invidious && docker compose up -d` — конфигурация применяется при перезапуске контейнеров.
|
||||
После изменения секретов в Vaultwarden: запустить `deploy-invidious-credentials.sh` снова.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
## Доступ и логины
|
||||
|
||||
- **Debian (CT 108):** логин `root`, пароль `Galene108!`.
|
||||
- **Debian (CT 108):** логин `root`. Пароль — в Vaultwarden (объект **CT_108_ROOT_PASSWORD**).
|
||||
- **Galene (веб):** https://call.katykhin.ru (через NPM → 192.168.1.108:8443). Вход в группы — по паролям, заданным в конфигах групп в `/opt/galene-data/groups/` (операторы и участники).
|
||||
|
||||
---
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
- **Доступ:** SSH под пользователем **admin** (не root): `ssh admin@192.168.1.200` с хоста 192.168.1.150 или из LAN. Для выполнения команд с правами root: `sudo ...`.
|
||||
|
||||
**Диски:**
|
||||
- **Корневой диск** (sda1): 35 GB, занято **~29 GB (87%)** — система, образы/кэш в пределах корня. **Критично:** мало свободного места; при росте логов или обновлениях возможны сбои. Следить за местом и логированием (см. TODO).
|
||||
- **Данные** (sdb1): 344 GB, смонтирован в **/mnt/data**, занято ~177 GB (55%). Здесь: библиотека Immich, БД PostgreSQL, Docker root, containerd, Ollama, данные deduper.
|
||||
- **Корневой диск** (sda1): 50 GB — система, образы/кэш в пределах корня. Логи Docker ограничены (см. ниже).
|
||||
- **Данные** (sdb1): 350 GB, смонтирован в **/mnt/data**. Здесь: библиотека Immich, БД PostgreSQL, Docker root, containerd, Ollama, данные deduper.
|
||||
|
||||
---
|
||||
|
||||
@@ -184,10 +184,10 @@ sudo resize2fs /dev/sdb1
|
||||
## Логи и ротация
|
||||
|
||||
- **Базовая политика (как в LXC):** на ВМ настроен logrotate `/etc/logrotate.d/homelab-lxc.conf` — 14 дней, 50 MB, 5 архивов, сжатие (системные логи в `/var/log`). На ВМ 200 пакет `logrotate` был установлен вручную (в образе по умолчанию не было); после установки активен таймер `logrotate.timer`. Подробнее: [Logrotate — базовая политика homelab](../maintenance/logrotate/README.md).
|
||||
- **Docker:** данные Docker (образы, контейнеры, overlay) хранятся в **/mnt/data/docker** (Docker Root Dir). Логи контейнеров — драйвер **json-file** без ограничения размера и количества файлов (Config:{} у immich_server и immich_postgres). При активной работе логи могут разрастаться и занимать место на **корневом** разделе (если логи пишутся на корень) или в overlay на /mnt/data — уточнить расположение логов контейнеров (часто в /mnt/data/docker/containers). В любом случае ограничение логов не задано (см. TODO).
|
||||
- **Docker:** данные Docker (образы, контейнеры, overlay) хранятся в **/mnt/data/docker** (Docker Root Dir). Логи контейнеров — драйвер **json-file** с ограничениями в `/etc/docker/daemon.json`: `max-size: "10m"`, `max-file: "3"` (до 30 MB на контейнер). Логи пишутся в `/mnt/data/docker/containers`.
|
||||
- **Системный logrotate:** стандартные правила (apt, dpkg, cloud-init, unattended-upgrades, wtmp) плюс homelab-lxc.conf. Отдельных правил для Immich или Docker нет.
|
||||
|
||||
**Риск:** корневой диск заполнен на 87%. Рост логов, обновления и кэш могут привести к нехватке места. Необходимо ограничить логи Docker и следить за местом на корне (см. TODO).
|
||||
Корневой диск расширен до 50 GB; логи Docker ограничены.
|
||||
|
||||
---
|
||||
|
||||
@@ -208,18 +208,17 @@ sudo resize2fs /dev/sdb1
|
||||
## Уязвимости и риски
|
||||
|
||||
1. **Секреты в .env:** В `/opt/immich/.env` и `/opt/immich-deduper/.env` хранятся пароли БД, API-ключи (IMMICH_API_KEY, GEMINI_API_KEY), креды для deduper (PSQL_*). Файлы не должны попадать в публичный репозиторий. Ограничить права (chmod 600), хранить бэкапы в защищённом месте.
|
||||
2. **Корневой диск 87%:** Критично мало свободного места. При 100% возможны сбои обновлений и работы сервисов. Срочно: освободить место и/или перенести часть данных на /mnt/data, ограничить логи Docker (см. TODO).
|
||||
3. **Логи Docker без лимитов:** Ротация не настроена — возможен рост логов и заполнение диска.
|
||||
4. **Доступ по портам:** Сервисы доступны по 2283, 2284, 8001, 8501, 8086 из LAN. Снаружи доступ только через NPM (https://immich.katykhin.ru → 2283). Не пробрасывать порты напрямую в интернет.
|
||||
5. **GPU и PCI-passthrough:** ВМ использует hostpci0 (VGA). Убедиться, что драйверы NVIDIA и доступ к GPU корректны для immich_machine_learning.
|
||||
2. **Корневой диск:** Расширен до 50 GB; логи Docker ограничены (10m × 3 файла на контейнер). Следить за местом при обновлениях.
|
||||
3. **Доступ по портам:** Сервисы доступны по 2283, 2284, 8001, 8501, 8086 из LAN. Снаружи доступ только через NPM (https://immich.katykhin.ru → 2283). Не пробрасывать порты напрямую в интернет.
|
||||
4. **GPU и PCI-passthrough:** ВМ использует hostpci0 (VGA). Убедиться, что драйверы NVIDIA и доступ к GPU корректны для immich_machine_learning.
|
||||
|
||||
---
|
||||
|
||||
## TODO по ВМ 200
|
||||
|
||||
- [x] **Базовая политика logrotate:** для системных логов настроена (homelab-lxc.conf — 14 дней, 50 MB, 5 архивов, как в LXC). См. [Logrotate — базовая политика homelab](../maintenance/logrotate/README.md).
|
||||
- [ ] **Корневой диск:** Снизить использование корня (87%). Варианты: перенести логи Docker на /mnt/data (если сейчас пишутся на корень), очистить старые образы/кэш (`docker system prune` с осторожностью), увеличить размер корневого диска ВМ в Proxmox. Настроить мониторинг и оповещение при заполнении >90%.
|
||||
- [ ] **Логи Docker:** Включить ограничение размера логов для всех контейнеров Immich и deduper: в `docker-compose.yml` добавить для каждого сервиса `logging: driver: json-file options: max-size: "100m" max-file: "3"` или задать default в `/etc/docker/daemon.json`. Убедиться, что Docker Root Dir остаётся на /mnt/data и логи не пишутся на корень. После изменений перезапустить контейнеры.
|
||||
- [x] **Корневой диск:** Расширен до 50 GB (было 35 GB). Логи Docker ограничены.
|
||||
- [x] **Логи Docker:** В `/etc/docker/daemon.json` заданы `log-driver: json-file`, `max-size: "10m"`, `max-file: "3"`. Логи в /mnt/data/docker/containers.
|
||||
- [ ] **Права на конфиги:** Ограничить доступ к .env (chmod 600), не коммитить в публичные репозитории.
|
||||
- [ ] **Резервное копирование:** Регулярный бэкап критичных данных (оценка размеров на момент документации):
|
||||
- **`/mnt/data/library`** — библиотека Immich (фото, видео, превью). ~148 GB. Основной объём; бэкап обязателен (внешний диск, сетевое хранилище).
|
||||
|
||||
157
docs/containers/host-proxmox.md
Normal file
157
docs/containers/host-proxmox.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Хост Proxmox (192.168.1.150)
|
||||
|
||||
Описание хоста Proxmox VE: скрипты, systemd-сервисы, пути и демоны. Контейнеры и ВМ описаны в отдельных статьях (container-100.md и т.д.).
|
||||
|
||||
---
|
||||
|
||||
## Общие сведения
|
||||
|
||||
- **IP:** 192.168.1.150/24
|
||||
- **Доступ:** `ssh root@192.168.1.150`
|
||||
- **Роль:** гипервизор (LXC + KVM), точка запуска бэкапов, деплой секретов в контейнеры
|
||||
|
||||
---
|
||||
|
||||
## Диски
|
||||
|
||||
| Устройство | Тип | Размер | Использование |
|
||||
|--------------|------|--------|----------------------------------|
|
||||
| /dev/nvme0n1 | NVMe | 256 GB | LVM (система, local-lvm) |
|
||||
| /dev/sda | HDD | 2 TB | ZFS |
|
||||
| /dev/sdb | SSD | 2 TB | ext4, /mnt/backup |
|
||||
| /dev/sdc | HDD | 2 TB | ZFS (RAID1 с sda) |
|
||||
| /dev/sdd | HDD | 8 TB | ext4 (внешний) |
|
||||
|
||||
---
|
||||
|
||||
## Каталог скриптов: /root/scripts/
|
||||
|
||||
Скрипты копируются из репозитория в `/root/scripts/` на хосте.
|
||||
|
||||
### Бэкапы (backup-*)
|
||||
|
||||
| Скрипт | Таймер | Назначение |
|
||||
|--------|--------|------------|
|
||||
| backup-vps-miran.sh | 01:00 | VPS Миран: БД бота, voice_users, S3 |
|
||||
| backup-ct101-pgdump.sh | 01:15 | Nextcloud PostgreSQL |
|
||||
| backup-immich-photos.sh | 01:30 | rsync фото Immich с VM 200 |
|
||||
| backup-vps-mtproto.sh | 01:45 | Конфиги MTProto (VPS DE) |
|
||||
| backup-etc-pve.sh | 02:15 | /etc/pve, interfaces, hosts, resolv.conf |
|
||||
| backup-ct104-pgdump.sh | 02:30 | Paperless PostgreSQL |
|
||||
| backup-vaultwarden-data.sh | 02:45 | Данные Vaultwarden |
|
||||
| backup-ct103-gitea-pgdump.sh | 03:00 | Gitea PostgreSQL |
|
||||
| backup-vm200-pgdump.sh | 03:15 | Immich PostgreSQL (SSH на VM 200) |
|
||||
| backup-ct105-vectors.sh | 03:30 | Векторы RAG (vectors.npz) |
|
||||
| backup-restic-yandex.sh | 04:00 | restic → Yandex (без photos) |
|
||||
| backup-restic-yandex-photos.sh | 04:10 | restic → Yandex (только photos) |
|
||||
|
||||
### Дашборд мониторинга
|
||||
|
||||
| Компонент | Назначение |
|
||||
|-----------|------------|
|
||||
| homelab-dashboard.service | Дашборд homelab (хост, контейнеры, сервисы) на порту 19998 |
|
||||
| /root/scripts/dashboard/ | Скрипты: dashboard-exporter.py, dashboard-server.py, index.html |
|
||||
| deploy-dashboard.sh | Деплой дашборда на хост |
|
||||
| add-to-homepage.sh | Добавить ссылку в Homepage (CT 103) |
|
||||
|
||||
**URL:** http://192.168.1.150:19998
|
||||
|
||||
### Мониторинг и уведомления
|
||||
|
||||
| Скрипт | Назначение |
|
||||
|--------|------------|
|
||||
| healthcheck-ping.sh | Ping Healthchecks (04:35) — Dead man's switch |
|
||||
| watchdog-timers.sh | Проверка .ok файлов (12:00), алерт в Telegram при отсутствии |
|
||||
| smartd-notify.sh | Вызывается smartd при проблемах с дисками |
|
||||
| notify-telegram.sh | Общий шлюз уведомлений в Telegram |
|
||||
| notify-vzdump-success.sh | Уведомление после успешного vzdump (по systemd path) |
|
||||
|
||||
### Деплой (deploy-*)
|
||||
|
||||
Скрипты разворачивают секреты из Vaultwarden в контейнеры и на VPS:
|
||||
|
||||
| Скрипт | Куда |
|
||||
|--------|------|
|
||||
| deploy-beget-credentials.sh | CT 100 (certbot) |
|
||||
| deploy-nextcloud-credentials.sh | CT 101 |
|
||||
| deploy-gitea-credentials.sh | CT 103 |
|
||||
| deploy-paperless-credentials.sh | CT 104 |
|
||||
| deploy-rag-credentials.sh | CT 105 |
|
||||
| deploy-invidious-credentials.sh | CT 107 |
|
||||
| deploy-galene-credentials.sh | CT 108 |
|
||||
| deploy-wireguard-credentials.sh | CT 109 |
|
||||
| deploy-immich-credentials.sh | VM 200 |
|
||||
| deploy-vpn-route-check.sh | CT 100 (vpn-route-check) |
|
||||
|
||||
### Прочее
|
||||
|
||||
| Скрипт | Назначение |
|
||||
|--------|------------|
|
||||
| immich-pgdump-remote.sh | Копируется на VM 200, вызывается backup-vm200-pgdump.sh по SSH |
|
||||
| restore-one-vzdump-from-restic.sh | Восстановление одного vzdump из Yandex |
|
||||
| verify-restore-level1.sh, verify-vzdump-level2.sh | Проверка восстановления |
|
||||
| backup-setup-sdb1-mount.sh | Монтирование /dev/sdb1 в /mnt/backup |
|
||||
| setup-vps-miran-backup-on-proxmox.sh | Настройка бэкапа VPS Миран на хосте |
|
||||
| npm-add-proxy.sh, npm-add-proxy-vault.sh, npm-cert-cloud.sh | Вспомогательные для NPM |
|
||||
|
||||
---
|
||||
|
||||
## Systemd: таймеры и сервисы
|
||||
|
||||
Unit-файлы лежат в репозитории в `scripts/systemd/`, на хосте — в `/etc/systemd/system/`.
|
||||
|
||||
### Бэкапы (backup-*.timer)
|
||||
|
||||
Все бэкапы запускаются через systemd timers (cron не используется). Расписание: [backup-howto.md](../backup/backup-howto.md).
|
||||
|
||||
После успешного выполнения сервис создаёт файл `/var/run/backup-<name>.ok` с timestamp.
|
||||
|
||||
### Watchdog (backup-watchdog-timers.timer)
|
||||
|
||||
Ежедневно в **12:00** запускается `watchdog-timers.sh`: проверяет, что все `.ok` файлы свежие (не старше 24 ч). При отсутствии или устаревании — уведомление в Telegram.
|
||||
|
||||
### Healthcheck ping (backup-healthcheck-ping.timer)
|
||||
|
||||
Ежедневно в **04:35** — ping в Healthchecks (Dead man's switch). Если бэкапы не прошли и ping не отправился, Healthchecks шлёт алерт.
|
||||
|
||||
### Vzdump (notify-vzdump-success)
|
||||
|
||||
Задание vzdump настраивается в Proxmox UI. После успешного выполнения срабатывает path-юнит `notify-vzdump-success.service` → уведомление в Telegram.
|
||||
|
||||
### Проверка восстановления (verify-*)
|
||||
|
||||
Таймеры для периодической проверки restic и vzdump: `verify-restore-level1-*`, `verify-vzdump-level2`.
|
||||
|
||||
---
|
||||
|
||||
## Ключевые пути
|
||||
|
||||
| Путь | Назначение |
|
||||
|------|------------|
|
||||
| /mnt/backup/ | Локальные бэкапы (см. [backup-howto](../backup/backup-howto.md)) |
|
||||
| /var/run/backup-*.ok | Healthcheck-файлы для watchdog (timestamp последнего успешного запуска) |
|
||||
| /root/.healthchecks.env | URL и UUID для healthcheck-ping |
|
||||
| /root/.bw-master | Мастер-пароль Bitwarden CLI (chmod 600; для restic, pg_dump) |
|
||||
| /root/.restic-yandex.env | Переменные restic (репозиторий, ключи) |
|
||||
| /etc/smartd.conf | Конфигурация smartd |
|
||||
| /etc/pve/ | Конфиги Proxmox (бэкапятся в backup-etc-pve) |
|
||||
|
||||
---
|
||||
|
||||
## Демоны и сервисы
|
||||
|
||||
| Сервис | Назначение |
|
||||
|--------|------------|
|
||||
| smartd | Мониторинг SMART дисков, при проблемах — smartd-notify.sh → Telegram |
|
||||
| pveproxy, pvedaemon | Proxmox API и веб-интерфейс |
|
||||
| corosync, pve-cluster | Кластер Proxmox (при одномузловой установке — локально) |
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [Архитектура](../architecture/architecture.md) — обзор, контейнеры, поток запросов
|
||||
- [Бэкапы](../backup/backup-howto.md) — что, куда, когда, восстановление
|
||||
- [Схема сети](../network/network-topology.md) — топология, зависимости
|
||||
- [smartd](../monitoring/smartd-setup.md) — мониторинг дисков
|
||||
- [Healthchecks](../vps/healthchecks-miran-setup.md) — Dead man's switch на VPS Миран
|
||||
146
docs/monitoring/dashboard-plan.md
Normal file
146
docs/monitoring/dashboard-plan.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# План реализации дашборда мониторинга homelab
|
||||
|
||||
Дашборд для Netdata (http://192.168.1.150:19999) с блоками: хост, контейнеры, критические сервисы.
|
||||
|
||||
---
|
||||
|
||||
## Текущее состояние (по результатам проверки на сервере)
|
||||
|
||||
### Netdata
|
||||
- **Версия:** v2.9.0
|
||||
- **Режим:** локальный, Cloud отключён
|
||||
- **API:** http://localhost:19999/api/v1/ — доступен
|
||||
|
||||
### Доступные метрики
|
||||
|
||||
| Блок | Метрика | Chart / источник | Статус |
|
||||
|------|---------|------------------|--------|
|
||||
| **Хост** | CPU total | `system.cpu` (user, system, nice, iowait, …) | ✅ |
|
||||
| | RAM total | `system.ram` | ✅ |
|
||||
| | Disk usage | `disk_space./`, `disk_space./mnt/backup`, `disk_space./mnt/nextcloud-hdd`, `disk_space./tank`, … (в API: URL-encode слэши) | ✅ |
|
||||
| | iowait | `system.cpu` dimension `iowait` | ✅ |
|
||||
| | load | `system.load` (load1, load5, load15) | ✅ |
|
||||
| **Контейнеры** | CPU % | `cgroup_<name>.cpu_limit` (used) | ✅ |
|
||||
| | RAM % | `cgroup_<name>.mem_utilization` | ✅ |
|
||||
| | Disk % | скрипт `pct exec ID -- df -P /` | ✅ кастомный экспортер |
|
||||
| | OOM count | `/sys/fs/cgroup/lxc/ID/memory.events` (oom_kill) | ✅ кастомный экспортер |
|
||||
| **Сервисы** | Immich, Nextcloud, nginx, VPN | ссылки на charts Netdata | ✅ без response time/connections |
|
||||
|
||||
### Контейнеры в cgroups (по данным Netdata)
|
||||
- `nginx` (CT 100)
|
||||
- `nextcloud` (CT 101)
|
||||
- `gitea` (CT 103)
|
||||
- `paperless` (CT 104)
|
||||
- `rag-service` (CT 105)
|
||||
- `misc` (CT 107, Invidious)
|
||||
- `galene` (CT 108)
|
||||
- `local-vpn` (CT 109)
|
||||
- `qemu_immich` (VM 200)
|
||||
|
||||
---
|
||||
|
||||
## Решения (по ответам пользователя)
|
||||
|
||||
1. **Disk % по контейнерам** — в приоритете. I/O не нужен. Реализация: скрипт на хосте, `pct exec ID -- df -P /` для каждого LXC, VM 200 — отдельно (`qm guest exec` или аналог).
|
||||
2. **OOM** — `cgroup memory.events` (oom_kill) по каждому контейнеру. Путь: `/sys/fs/cgroup/lxc/ID/memory.events` (LXC), для VM — cgroup QEMU.
|
||||
3. **Response time / open connections** — отложено, не требуется.
|
||||
4. **Размещение** — на хосте (192.168.1.150).
|
||||
5. **Netdata Cloud** — не рассматривается.
|
||||
|
||||
---
|
||||
|
||||
## Варианты реализации дашборда
|
||||
|
||||
### Вариант A: Netdata Cloud — не используется (Cloud отключён)
|
||||
|
||||
### Вариант B: Кастомная HTML-страница (выбран)
|
||||
- Страница на хосте (192.168.1.150), которая:
|
||||
- запрашивает Netdata API (`/api/v1/data?chart=...`)
|
||||
- рендерит блоки: хост, таблица контейнеров, сервисы
|
||||
- Плюсы: полный контроль, работает без Cloud
|
||||
- Минусы: нужна разработка и хостинг страницы
|
||||
|
||||
### Вариант C: Доработка стандартного дашборда Netdata
|
||||
- `dashboard_info.js` — изменение порядка/группировки charts
|
||||
- Плюсы: используем встроенный UI
|
||||
- Минусы: ограниченная кастомизация, в v2 подход мог измениться
|
||||
|
||||
---
|
||||
|
||||
## Рекомендуемый план (поэтапно)
|
||||
|
||||
### Этап 1: Дашборд на базе Netdata API (Вариант B)
|
||||
Создать кастомную HTML-страницу с тремя блоками.
|
||||
|
||||
**Блок 1 — Хост**
|
||||
- CPU total: `system.cpu` (сумма user+system или 100-idle)
|
||||
- RAM total: `system.ram` (used, cached, free)
|
||||
- Disk usage: `disk_space./`, `disk_space./mnt/backup`, `disk_space./mnt/nextcloud-hdd`, `disk_space./tank` (avail/used %)
|
||||
- iowait: `system.cpu` dimension iowait
|
||||
- load: `system.load` (load15)
|
||||
|
||||
**Блок 2 — Контейнеры (таблица)**
|
||||
- Колонки: имя, CPU %, RAM %, Disk %, OOM count
|
||||
- CPU/RAM: `cgroup_<name>.cpu_limit`, `cgroup_<name>.mem_utilization` (Netdata API)
|
||||
- Disk %: кастомный API (скрипт `pct exec ID -- df -P /` + парсинг)
|
||||
- OOM: кастомный API (LXC: `/sys/fs/cgroup/lxc/ID/memory.events`, VM 200: `/sys/fs/cgroup/qemu.slice/200.scope/memory.events` → oom_kill)
|
||||
|
||||
**Блок 3 — Критические сервисы**
|
||||
- Immich, Nextcloud, nginx, VPN — ссылки на charts Netdata (cgroup_*, app.nginx_*)
|
||||
- Response time / open connections — не требуются
|
||||
|
||||
### Этап 2: Кастомный экспортер (скрипт + HTTP API)
|
||||
Скрипт на хосте, запускаемый по таймеру или по запросу:
|
||||
- **Disk %:** для каждого LXC (100–109) — `pct exec ID -- df -P /`; для VM 200 — `qm guest exec` или fallback (lvs/zfs)
|
||||
- **OOM:** чтение `oom_kill` из `/sys/fs/cgroup/lxc/ID/memory.events` (LXC), `/sys/fs/cgroup/qemu.slice/200.scope/memory.events` (VM 200)
|
||||
- Отдача JSON на HTTP (например, порт 19998 или через nginx на хосте)
|
||||
|
||||
### Этап 3: Размещение и интеграция
|
||||
- Дашборд: статика на хосте (nginx или python -m http.server), запросы к Netdata API (localhost:19999) и кастомному API
|
||||
- Добавить ссылку в Homepage (services.yaml)
|
||||
|
||||
---
|
||||
|
||||
## VM 200 (Immich) — RAM после увеличения
|
||||
|
||||
При увеличении RAM с 6 до 10 GB «потребление» может визуально «упасть» по нескольким причинам:
|
||||
|
||||
1. **Процент vs абсолютное значение** — 32% от 10 GB ≈ 3.2 GB. Та же нагрузка при 6 GB давала бы ~53%. Дашборд показывает RAM % (cgroup mem_utilization).
|
||||
2. **Сброс кэша** — при нехватке памяти гость держит кэш; после добавления RAM ядро может освободить кэш, и «used» уменьшается.
|
||||
3. **Balloon** — virtio-balloon мог забирать память при 6 GB; после увеличения лимита balloon отдаёт память гостю, но реальное использование приложений может остаться ~3 GB.
|
||||
|
||||
**Проверка:** `qm guest exec 200 -- free -h` (требует qemu-guest-agent в гостевой ОС) — смотреть `Mem: used` внутри гостя.
|
||||
|
||||
---
|
||||
|
||||
## Реализовано (2026-02-28)
|
||||
|
||||
- **URL дашборда:** http://192.168.1.150:19998
|
||||
- **Ссылка в Homepage:** добавлена (Сервисы → Homelab Dashboard)
|
||||
- **Скрипты:** `scripts/dashboard/` (exporter, server, index.html, deploy, add-to-homepage)
|
||||
- **Systemd:** `homelab-dashboard.service` (порт 19998)
|
||||
|
||||
---
|
||||
|
||||
## Маппинг CT/VM → cgroup name (Netdata)
|
||||
|
||||
| ID | Назначение | cgroup name |
|
||||
|----|------------|-------------|
|
||||
| 100 | nginx | cgroup_nginx |
|
||||
| 101 | nextcloud | cgroup_nextcloud |
|
||||
| 103 | gitea | cgroup_gitea |
|
||||
| 104 | paperless | cgroup_paperless |
|
||||
| 105 | rag-service | cgroup_rag-service |
|
||||
| 107 | misc (Invidious) | cgroup_misc |
|
||||
| 108 | galene | cgroup_galene |
|
||||
| 109 | local-vpn | cgroup_local-vpn |
|
||||
| 200 | immich (VM) | cgroup_qemu_immich |
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [netdata-proxmox-setup.md](netdata-proxmox-setup.md) — установка и алерты
|
||||
- [smartd-setup.md](smartd-setup.md) — SMART дисков
|
||||
- [container-100](../containers/container-100.md) — NPM, log-dashboard
|
||||
- [architecture](../architecture/architecture.md) — обзор контейнеров
|
||||
211
docs/monitoring/netdata-proxmox-setup.md
Normal file
211
docs/monitoring/netdata-proxmox-setup.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Netdata на хосте Proxmox
|
||||
|
||||
Мониторинг CPU, RAM, дисков, load average, swap. Алерты в Telegram.
|
||||
|
||||
---
|
||||
|
||||
## Доступ
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **URL** | http://192.168.1.150:19999 |
|
||||
| **Режим** | Локальный, анонимный (Cloud отключён) |
|
||||
|
||||
---
|
||||
|
||||
## Установка
|
||||
|
||||
На хосте Proxmox (root):
|
||||
|
||||
```bash
|
||||
# Официальный установщик
|
||||
wget -O /tmp/netdata-kickstart.sh https://get.netdata.cloud/kickstart.sh
|
||||
sh /tmp/netdata-kickstart.sh --stable-channel --disable-telemetry
|
||||
```
|
||||
|
||||
Или через пакетный менеджер (если доступен):
|
||||
|
||||
```bash
|
||||
apt update && apt install -y netdata
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация Telegram
|
||||
|
||||
Редактировать: `/etc/netdata/health_alarm_notify.conf`
|
||||
|
||||
```bash
|
||||
cd /etc/netdata
|
||||
./edit-config health_alarm_notify.conf
|
||||
```
|
||||
|
||||
Добавить/изменить:
|
||||
|
||||
```
|
||||
SEND_TELEGRAM="YES"
|
||||
TELEGRAM_BOT_TOKEN="<токен из Vaultwarden: HOME_BOT_TOKEN>"
|
||||
DEFAULT_RECIPIENT_TELEGRAM="<chat_id из Vaultwarden: RESTIC.TELEGRAM_SELF_CHAT_ID>"
|
||||
```
|
||||
|
||||
Креды можно взять из Vaultwarden (объекты HOME_BOT_TOKEN, RESTIC) или из `/root/.telegram-notify.env`.
|
||||
|
||||
---
|
||||
|
||||
## Алерты (health.d)
|
||||
|
||||
Файлы в `/etc/netdata/health.d/`. Создать или переопределить:
|
||||
|
||||
### cpu.conf — CPU > 90% более 10 минут
|
||||
|
||||
```conf
|
||||
# Переопределение: CPU > 90% = warning
|
||||
template: cpu_usage
|
||||
on: system.cpu
|
||||
lookup: average -10m percentage of usage
|
||||
warn: $this > 90
|
||||
crit: $this > 95
|
||||
```
|
||||
|
||||
### ram.conf — RAM > 90%
|
||||
|
||||
```conf
|
||||
template: ram_usage
|
||||
on: system.ram
|
||||
lookup: average -10m percentage of used
|
||||
warn: $this > 90
|
||||
crit: $this > 95
|
||||
```
|
||||
|
||||
### load.conf — Load average > cores × 2
|
||||
|
||||
```conf
|
||||
# Load average: warn если load > 2 × число ядер
|
||||
# Число ядер: nproc или lscpu
|
||||
template: load_average
|
||||
on: system.load
|
||||
lookup: average -10m of load15
|
||||
# Порог задаётся вручную под хост (cores × 2). Пример для 8 ядер: 16
|
||||
warn: $this > 16
|
||||
crit: $this > 24
|
||||
```
|
||||
|
||||
**Важно:** заменить `16` и `24` на `cores × 2` и `cores × 3` для вашего хоста. Узнать ядра: `nproc`.
|
||||
|
||||
### swap.conf — Swap > 0 стабильно
|
||||
|
||||
```conf
|
||||
template: swap_usage
|
||||
on: system.swap
|
||||
lookup: average -10m percentage of used
|
||||
warn: $this > 0
|
||||
crit: $this > 10
|
||||
```
|
||||
|
||||
### disk.conf — Диск > 80% (avail < 20%)
|
||||
|
||||
Мониторить: `/` (root, NVMe), `/mnt/backup` (sdb), внешний диск (sdd). Netdata использует `percentage of avail` — warn при avail < 20% (т.е. used > 80%).
|
||||
|
||||
```conf
|
||||
# Шаблон для важных дисков: warn при avail < 20%, crit при avail < 10%
|
||||
template: disk_space_critical
|
||||
on: disk.space
|
||||
lookup: max -1m percentage of avail
|
||||
warn: $this < 20
|
||||
crit: $this < 10
|
||||
```
|
||||
|
||||
Или для конкретных путей (chart ID: disk_space._ с подчёркиваниями вместо слешей):
|
||||
|
||||
```conf
|
||||
# / (root, NVMe)
|
||||
alarm: disk_space_root
|
||||
on: disk_space._
|
||||
lookup: max -1m percentage of avail
|
||||
chart labels: mount_point=/
|
||||
warn: $this < 20
|
||||
crit: $this < 10
|
||||
|
||||
# /mnt/backup (sdb)
|
||||
alarm: disk_space_backup
|
||||
on: disk_space._mnt_backup
|
||||
lookup: max -1m percentage of avail
|
||||
warn: $this < 20
|
||||
crit: $this < 10
|
||||
```
|
||||
|
||||
Узнать точные chart ID: `curl -s "http://localhost:19999/api/v1/charts" | grep disk_space`
|
||||
|
||||
---
|
||||
|
||||
## SMART (smartmontools)
|
||||
|
||||
Для мониторинга SMART через Netdata:
|
||||
|
||||
```bash
|
||||
apt install -y smartmontools
|
||||
```
|
||||
|
||||
Плагин `smartd` в Netdata автоматически обнаруживает диски. Дополнительно см. [smartd-setup.md](smartd-setup.md).
|
||||
|
||||
---
|
||||
|
||||
## Применение изменений
|
||||
|
||||
```bash
|
||||
netdatacli reload-health
|
||||
# или
|
||||
systemctl restart netdata
|
||||
```
|
||||
|
||||
Тест алертов:
|
||||
|
||||
```bash
|
||||
sudo su -s /bin/bash netdata
|
||||
export NETDATA_ALARM_NOTIFY_DEBUG=1
|
||||
/usr/libexec/netdata/plugins.d/alarm-notify.sh test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Мониторинг контейнеров и VM
|
||||
|
||||
Netdata на хосте видит общие метрики (CPU, RAM, диск хоста). Для детального мониторинга каждого LXC/VM:
|
||||
|
||||
- **Вариант 1:** Netdata parent/child — установить агент в каждый CT/VM, связать с родителем.
|
||||
- **Вариант 2:** Один Netdata на хосте — мониторит хост и агрегирует по контейнерам через cgroups (если включено).
|
||||
|
||||
Для homelab обычно достаточно мониторинга хоста. При необходимости — см. [Netdata Parent-Child](https://learn.netdata.cloud/docs/agent/streaming).
|
||||
|
||||
---
|
||||
|
||||
## Отключение Netdata Cloud
|
||||
|
||||
Если не нужен trial/Cloud:
|
||||
|
||||
1. **Полное отключение:** удалить `/var/lib/netdata/cloud.d/` (токены, ключи) и создать заново только `cloud.conf`:
|
||||
```bash
|
||||
rm -rf /var/lib/netdata/cloud.d
|
||||
mkdir -p /var/lib/netdata/cloud.d
|
||||
echo -e "[global]\nenabled = no" > /var/lib/netdata/cloud.d/cloud.conf
|
||||
chown -R netdata:netdata /var/lib/netdata/cloud.d
|
||||
systemctl restart netdata
|
||||
```
|
||||
|
||||
2. **Локальный дашборд** — http://host:19999 (анонимный доступ, без Cloud). Не использовать app.netdata.cloud — иначе снова появится claim/Cloud.
|
||||
|
||||
---
|
||||
|
||||
## Дашборд homelab
|
||||
|
||||
Кастомный дашборд с метриками хоста, контейнеров и сервисов: **http://192.168.1.150:19998**
|
||||
|
||||
Ссылка добавлена в Homepage (Сервисы → Homelab Dashboard). Деплой: `scripts/dashboard/deploy-dashboard.sh`. Подробнее: [dashboard-plan.md](dashboard-plan.md).
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [dashboard-plan.md](dashboard-plan.md) — план и реализация кастомного дашборда
|
||||
- [smartd-setup.md](smartd-setup.md) — SMART и диски
|
||||
- [backup-howto](../backup/backup-howto.md) — бэкапы
|
||||
109
docs/monitoring/smartd-setup.md
Normal file
109
docs/monitoring/smartd-setup.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# SMART и smartd — мониторинг дисков
|
||||
|
||||
Настройка `smartd` для мониторинга дисков Proxmox: NVMe, HDD, SSD. При отклонениях — уведомление в Telegram через `notify-telegram.sh`.
|
||||
|
||||
---
|
||||
|
||||
## Диски (из контекста homelab)
|
||||
|
||||
| Устройство | Тип | Размер | Использование |
|
||||
|--------------|------|--------|----------------------------------|
|
||||
| /dev/nvme0n1 | NVMe | 256 GB | LVM (система, local-lvm) |
|
||||
| /dev/sda | HDD | 2 TB | ZFS |
|
||||
| /dev/sdb | SSD | 2 TB | ext4, /mnt/backup |
|
||||
| /dev/sdc | HDD | 2 TB | ZFS (RAID1 с sda) |
|
||||
| /dev/sdd | HDD | 8 TB | ext4 (внешний) |
|
||||
|
||||
---
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
apt install -y smartmontools
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация smartd
|
||||
|
||||
Файл: `/etc/smartd.conf`. Текущая конфигурация на хосте:
|
||||
|
||||
```conf
|
||||
# NVMe (система)
|
||||
/dev/nvme0n1 -a -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
|
||||
# HDD sda (ZFS)
|
||||
/dev/sda -a -d ata -R 5 -R 197 -R 198 -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
|
||||
# SSD sdb (backup)
|
||||
/dev/sdb -a -d ata -R 5 -R 197 -R 198 -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
|
||||
# HDD sdc (ZFS)
|
||||
/dev/sdc -a -d ata -R 5 -R 197 -R 198 -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
|
||||
# HDD sdd (внешний, USB/SAT)
|
||||
/dev/sdd -a -d sat -R 5 -R 197 -R 198 -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `-a` — мониторить все атрибуты
|
||||
- `-d ata` / `-d sat` — тип устройства (ata для SATA, sat для USB/SAT)
|
||||
- `-R 5` — Reallocated_Sector_Ct
|
||||
- `-R 197` — Current_Pending_Sector
|
||||
- `-R 198` — Offline_Uncorrectable
|
||||
- `-W 4,45,55` — температура: delta 4°C, warn 45°C, crit 55°C
|
||||
- `-M exec` — выполнить скрипт при проблеме
|
||||
|
||||
---
|
||||
|
||||
## Скрипт уведомления в Telegram
|
||||
|
||||
Создать `/root/scripts/smartd-notify.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Вызывается smartd при обнаружении проблемы.
|
||||
# Аргументы: device, type (health/usage/fail), message
|
||||
# См. man smartd.conf (-M exec)
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
DEVICE="${1:-unknown}"
|
||||
# smartd передаёт полный вывод в stdin
|
||||
MSG=$(cat)
|
||||
SUMMARY="${2:-SMART problem}"
|
||||
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
"$NOTIFY_SCRIPT" "⚠️ SMART" "Диск $DEVICE: $SUMMARY
|
||||
|
||||
$MSG" || true
|
||||
fi
|
||||
|
||||
# Передать дальше в mail (если настроен)
|
||||
exit 0
|
||||
```
|
||||
|
||||
Сделать исполняемым: `chmod +x /root/scripts/smartd-notify.sh`
|
||||
|
||||
**Примечание:** smartd при `-M exec` передаёт в скрипт до 3 аргументов и stdin. Точный формат см. в `man smartd.conf` (раздел -M exec).
|
||||
|
||||
---
|
||||
|
||||
## Запуск smartd
|
||||
|
||||
```bash
|
||||
systemctl enable --now smartd
|
||||
systemctl status smartd
|
||||
```
|
||||
|
||||
Проверка вручную:
|
||||
|
||||
```bash
|
||||
smartctl -a /dev/sda
|
||||
smartctl -a /dev/nvme0n1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Интеграция с Netdata
|
||||
|
||||
Netdata имеет плагин smartmontools. После установки smartmontools и настройки smartd Netdata может отображать метрики SMART на дашборде. См. [netdata-proxmox-setup.md](netdata-proxmox-setup.md).
|
||||
@@ -132,7 +132,7 @@ flowchart TB
|
||||
│ VPS DE │ │ VPS US │ │ VPS Миран (СПБ) │
|
||||
│ 185.103.253.99 │ │ 147.45.124.117 │ │ 185.147.80.190 │
|
||||
│ AmneziaWG │ │ AmneziaWG │ │ coTURN (Galene), │
|
||||
│ (обход блок.) │ │ (обход блок.) │ │ боты, Prometheus │
|
||||
│ (обход блок.) │ │ (обход блок.) │ │ Healthchecks, боты │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
@@ -160,7 +160,7 @@ flowchart TB
|
||||
| **VM 200** | 192.168.1.200 | Immich, PostgreSQL, Redis, ML, deduper, Power Tools, Public Share | immich.katykhin.ru, immich-pt.katykhin.ru, share.katykhin.ru |
|
||||
| **VPS DE** | 185.103.253.99 | AmneziaWG (обход блокировок) | Туннель с роутера (10.8.1.x) |
|
||||
| **VPS US** | 147.45.124.117 | AmneziaWG (второй выход) | Туннель с роутера |
|
||||
| **VPS Миран** | 185.147.80.190 | coTURN (STUN/TURN для Galene), боты, prod | call.katykhin.ru использует STUN/TURN |
|
||||
| **VPS Миран** | 185.147.80.190 | coTURN (STUN/TURN для Galene), Healthchecks, боты, prod | call.katykhin.ru (STUN/TURN), healthchecks.katykhin.ru |
|
||||
| **DNS** | Beget.com | Домен katykhin.ru, поддомены, API для DNS-01 | Все *.katykhin.ru |
|
||||
|
||||
---
|
||||
@@ -241,6 +241,7 @@ flowchart TB
|
||||
## Связь с другими документами
|
||||
|
||||
- [Архитектура и подключение](../architecture/architecture.md) — общее описание, таблица контейнеров, поток запросов.
|
||||
- [Хост Proxmox](../containers/host-proxmox.md) — скрипты, таймеры, пути на 192.168.1.150.
|
||||
- [Контейнер 100](../containers/container-100.md) — NPM, AdGuard, Homepage, порядок запуска.
|
||||
- [Контейнер 109](../containers/container-109.md) — WireGuard VPN (local-vpn), доступ к vault и LAN.
|
||||
- [Генерация .mobileconfig для WireGuard (On-Demand)](vpn-mobileconfig-wireguard.md) — как собрать профиль для iOS/macOS с автоматическим подключением вне дома.
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
chmod 600 /root/.secrets/certbot/beget.ini
|
||||
```
|
||||
|
||||
**Homelab (Vaultwarden):** креды хранятся в Vaultwarden (объект **beget**). Деплой с хоста Proxmox:
|
||||
```bash
|
||||
/root/scripts/deploy-beget-credentials.sh
|
||||
```
|
||||
Скрипт генерирует `beget.ini` из Vaultwarden, атомарно пушит в CT 100, ставит права 600 и pre-hook проверки. **Ротация:** сменил пароль в Vaultwarden → запустил `deploy-beget-credentials.sh` → готово.
|
||||
|
||||
3. **Запрос сертификата:**
|
||||
```bash
|
||||
certbot certonly \
|
||||
|
||||
287
docs/vaultwarden-secrets.md
Normal file
287
docs/vaultwarden-secrets.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
### Пример: Beget (certbot DNS-01, CT 100)
|
||||
|
||||
Скрипт `deploy-beget-credentials.sh` на Proxmox генерирует `beget.ini` из объекта **beget** (username → `dns_beget_api_username`, password → `dns_beget_api_password`), атомарно пушит в CT 100 (`beget.ini.tmp` → `mv` → `beget.ini`), ставит chmod 600. Pre-hook certbot проверяет наличие файла и права перед каждым renew. **Ротация:** сменил пароль в Vaultwarden → `deploy-beget-credentials.sh` → готово.
|
||||
|
||||
### Пример: Invidious (CT 107)
|
||||
|
||||
Скрипт `deploy-invidious-credentials.sh` генерирует `.env` из объекта **INVIDIOUS** (username, password, поля `SERVER_SECRET_KEY`, `HMAC_KEY`), атомарно пушит в CT 107, запускает `docker compose up -d --force-recreate`. **Ротация:** сменил пароль/ключи в Vaultwarden → запустил скрипт.
|
||||
|
||||
### Пример: Paperless (CT 104)
|
||||
|
||||
Скрипт `deploy-paperless-credentials.sh` генерирует `docker-compose.env` из объекта **PAPERLESS** (password = POSTGRES_PASSWORD; поля `PAPERLESS_URL`, `PAPERLESS_SECRET_KEY`, `PAPERLESS_TIME_ZONE`, `PAPERLESS_OCR_LANGUAGE`, `PAPERLESS_OCR_LANGUAGES`), пушит compose и env в CT 104, запускает `docker compose up -d --force-recreate`. **Ротация:** сменил пароль/ключи в Vaultwarden → запустил скрипт.
|
||||
|
||||
### Пример: RAG-service (CT 105)
|
||||
|
||||
Скрипт `deploy-rag-credentials.sh` генерирует `.env` из объекта **RAG_SERVICE** (поле `RAG_API_KEY`), атомарно пушит в CT 105, запускает `docker compose up -d --force-recreate`. **Перед первым запуском:** создать в Vaultwarden запись **RAG_SERVICE** (тип Login), добавить кастомное поле `RAG_API_KEY` (hidden) с текущим ключом из `/home/rag-service/.env`. **Ротация:** сменил ключ в Vaultwarden → запустил скрипт.
|
||||
|
||||
### Пример: Gitea (CT 103)
|
||||
|
||||
Скрипт `deploy-gitea-credentials.sh` генерирует `.env` из объекта **GITEA** (password = POSTGRES_PASSWORD; поле `GITEA_RUNNER_REGISTRATION_TOKEN`), пушит compose и env в CT 103, запускает `docker compose up -d --force-recreate`. **Ротация:** сменил пароль/токен в Vaultwarden → запустил скрипт.
|
||||
|
||||
### Пример: Nextcloud (CT 101)
|
||||
|
||||
Скрипт `deploy-nextcloud-credentials.sh` генерирует `.env` и `docker-compose.yml` из объекта **NEXTCLOUD** (password = POSTGRES_PASSWORD; поля `dbpassword`, `secret`, `passwordsalt`, `instanceid`), пушит в CT 101, обновляет config.php через occ, запускает compose. **Ротация:** сменил в Vaultwarden → запустил скрипт.
|
||||
|
||||
### Пример: Galene (CT 108)
|
||||
|
||||
Скрипт `deploy-galene-credentials.sh` берёт поле `config` (JSON ice-servers) из объекта **GALENE**, записывает в `/opt/galene-data/data/ice-servers.json`, перезапускает `galene.service`. **Ротация:** сменил TURN username/credential в Vaultwarden → запустил скрипт.
|
||||
|
||||
### Пример: Immich (VM 200)
|
||||
|
||||
Скрипт `deploy-immich-credentials.sh` генерирует `.env` для Immich и immich-deduper из объектов **IMMICH** и **IMMICH_DEDUPER**, пушит по SSH на VM 200, запускает compose. **Требования:** SSH без пароля root@Proxmox → admin@192.168.1.200. **Ротация:** сменил в Vaultwarden → запустил скрипт.
|
||||
|
||||
### Пример: WireGuard (CT 109)
|
||||
|
||||
Скрипт `deploy-wireguard-credentials.sh` берёт поле `wg0_conf` (полный конфиг) из объекта **LOCAL_VPN_SERVER_WG**, записывает в `/etc/wireguard/wg0.conf`, перезапускает `wg-quick@wg0`. **Перед первым запуском:** создать в Vaultwarden запись **LOCAL_VPN_SERVER_WG**, добавить кастомное поле `wg0_conf` (hidden) с содержимым текущего `/etc/wireguard/wg0.conf` (скопировать с CT 109). **Ротация:** сменил ключи в Vaultwarden → запустил скрипт.
|
||||
|
||||
### Пример: VPN Route Check (деплой с Proxmox в CT 100)
|
||||
|
||||
Скрипт `deploy-vpn-route-check.sh` на хосте Proxmox:
|
||||
|
||||
1. Разблокирует bw (или переиспользует сессию).
|
||||
2. Получает из объекта **localhost**: `ROUTER_TELNET_HOST` (кастомное поле), `ROUTER_TELNET_USER` (username), `ROUTER_TELNET_PASSWORD` (password).
|
||||
3. Генерирует `.env` во временный файл, атомарно (`mv .env.tmp .env`) пушит в CT 100.
|
||||
4. Запускает `docker compose up -d` в каталоге vpn-route-check.
|
||||
|
||||
Режим проверки без записи: `deploy-vpn-route-check.sh --dry-run`. Подробнее: [Контейнер 100](containers/container-100.md#7-vpn-route-check).
|
||||
|
||||
### 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**, **RAG_SERVICE**, **ADGUARD**, **NPM_ADMIN** и т.д. У записей типа «логин» — логин/пароль; у записей с множеством значений — кастомные поля (например `RESTIC_REPOSITORY`, `AWS_ACCESS_KEY_ID`, `RAG_API_KEY`).
|
||||
|
||||
**ADGUARD** — веб-интерфейс AdGuard Home (https://adguard.katykhin.ru): username = логин администратора, password = пароль. Тип записи: Login.
|
||||
|
||||
**NPM_ADMIN** — админка Nginx Proxy Manager (http://192.168.1.100:81): username = email (используется как identity при входе), password = пароль. Тип записи: Login. Скрипты `npm-add-proxy.sh`, `npm-add-proxy-vault.sh` используют `NPM_EMAIL` и `NPM_PASSWORD` — брать из этого объекта.
|
||||
|
||||
### Команды bw по объектам (для скриптов бэкапов и деплоя)
|
||||
|
||||
| Объект | Логин / пароль | Кастомные поля |
|
||||
|--------|----------------|----------------|
|
||||
| **ADGUARD** | `bw get username "ADGUARD"`, `bw get password "ADGUARD"` | — |
|
||||
| **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"` | GITEA_RUNNER_REGISTRATION_TOKEN и др. |
|
||||
| **HOME_BOT_TOKEN** | — | пароль = токен: `bw get password "HOME_BOT_TOKEN"` |
|
||||
| **localhost** | `bw get username "localhost"`, `bw get password "localhost"` | ROUTER_TELNET_HOST |
|
||||
| **NEXTCLOUD** | `bw get username "NEXTCLOUD"`, `bw get password "NEXTCLOUD"` | dbpassword, secret, passwordsalt, instanceid |
|
||||
| **NPM_ADMIN** | username = email, `bw get password "NPM_ADMIN"` | — |
|
||||
| **PAPERLESS** | `bw get password "PAPERLESS"` (= POSTGRES_PASSWORD) | PAPERLESS_SECRET_KEY, PAPERLESS_URL и др. |
|
||||
| **RESTIC** | — | RESTIC_BACKUP_KEY, RESTIC_REPOSITORY, AWS_*, TELEGRAM_SELF_CHAT_ID |
|
||||
| **VAULTWARDEN** | — | пароль = ADMIN_TOKEN: `bw get password "VAULTWARDEN"` |
|
||||
|
||||
Универсальный шаблон для поля: `bw get item "ИМЯ" | jq -r '.fields[] | select(.name=="ПОЛЕ") | .value'`
|
||||
|
||||
---
|
||||
|
||||
## См. также
|
||||
|
||||
- [Контейнер 103 (Gitea, Vaultwarden)](containers/container-103.md) — развёртывание Vaultwarden, порты, домен, NPM.
|
||||
- [backup-howto](backup/backup-howto.md) — общий план бэкапов и восстановления, в том числе данных Vaultwarden.
|
||||
112
docs/vps/healthchecks-miran-setup.md
Normal file
112
docs/vps/healthchecks-miran-setup.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Healthchecks на VPS Миран
|
||||
|
||||
Self-hosted [Healthchecks.io](https://healthchecks.io/) на VPS 185.147.80.190 — Dead man's switch для homelab. Если Proxmox не отправляет ping после окна бэкапов, Healthchecks шлёт алерт в Telegram.
|
||||
|
||||
---
|
||||
|
||||
## Доступ
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **URL** | https://healthchecks.katykhin.ru/healthchecks/ |
|
||||
| **Логин** | admin@katykhin.ru |
|
||||
| **Пароль** | в Vaultwarden (Healthchecks admin) |
|
||||
|
||||
Доступ настроен по домену. Telegram webhook требует валидный SSL — без домена с Let's Encrypt бот не отвечает на `/start`.
|
||||
|
||||
---
|
||||
|
||||
## Развёртывание (для переустановки)
|
||||
|
||||
### 1. Подготовка
|
||||
|
||||
```bash
|
||||
ssh -p 15722 deploy@185.147.80.190
|
||||
mkdir -p /home/prod/healthchecks
|
||||
cd /home/prod/healthchecks
|
||||
```
|
||||
|
||||
Скопировать из репозитория: `scripts/healthchecks-docker/docker-compose.yml`, `scripts/healthchecks-docker/.env.example` → `.env`
|
||||
|
||||
### 2. Конфигурация .env
|
||||
|
||||
```env
|
||||
SITE_ROOT=https://healthchecks.katykhin.ru/healthchecks
|
||||
SECRET_KEY=<openssl rand -hex 32>
|
||||
ALLOWED_HOSTS=healthchecks.katykhin.ru,185.147.80.190,localhost
|
||||
|
||||
DB_HOST=db
|
||||
DB_NAME=hc
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=<надёжный пароль>
|
||||
|
||||
TELEGRAM_TOKEN=<токен из Vaultwarden: HOME_BOT_TOKEN>
|
||||
TELEGRAM_BOT_NAME=<username бота из @BotFather, напр. Katykhinhomebot>
|
||||
|
||||
REGISTRATION_OPEN=False
|
||||
DEFAULT_FROM_EMAIL=healthchecks@katykhin.ru
|
||||
```
|
||||
|
||||
### 3. Запуск
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker-compose run web /opt/healthchecks/manage.py createsuperuser --email admin@katykhin.ru --password <password>
|
||||
docker-compose run web python /opt/healthchecks/manage.py settelegramwebhook
|
||||
```
|
||||
|
||||
### 4. Nginx
|
||||
|
||||
Отдельный server block для `healthchecks.katykhin.ru` с Let's Encrypt. Референс: `scripts/healthchecks-nginx-server.conf`. Proxy на 127.0.0.1:8000; нужны location для `/healthchecks/`, `/static/`, `/projects/`, `/accounts/`, `/integrations/`, `/ping/` и др. (Django редиректы без префикса).
|
||||
|
||||
### 5. DNS
|
||||
|
||||
A-запись: `healthchecks.katykhin.ru` → `185.147.80.190`. Сертификат: `certbot --nginx -d healthchecks.katykhin.ru`.
|
||||
|
||||
---
|
||||
|
||||
## Привязка Telegram к check
|
||||
|
||||
1. Войти в Healthchecks → **Integrations** → **Add Integration** → **Telegram**
|
||||
2. Писать **своему** боту (из TELEGRAM_TOKEN), не @HealthchecksBot
|
||||
3. В Telegram: `/start` боту → перейти по ссылке → **Connect** в веб-интерфейсе
|
||||
|
||||
Check **homelab-backups** (UUID: 9451b52b-89f5-4a6c-b922-247a775bbf45).
|
||||
|
||||
---
|
||||
|
||||
## Ping с Proxmox
|
||||
|
||||
Скрипт `/root/scripts/healthcheck-ping.sh`, таймер `backup-healthcheck-ping.timer` — 04:35 ежедневно.
|
||||
|
||||
Конфиг `/root/.healthchecks.env`:
|
||||
|
||||
```env
|
||||
HEALTHCHECKS_URL=https://healthchecks.katykhin.ru/healthchecks
|
||||
HEALTHCHECKS_HOMELAB_UUID=<uuid из Healthchecks>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Смена пароля без SMTP
|
||||
|
||||
Healthchecks требует SMTP для смены пароля через веб. Без SMTP — через Django:
|
||||
|
||||
```bash
|
||||
cd /home/prod/healthchecks
|
||||
docker-compose run web python /opt/healthchecks/manage.py shell -c "
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
u = User.objects.get(email='admin@katykhin.ru')
|
||||
u.set_password('NEW_PASSWORD')
|
||||
u.save()
|
||||
print('OK')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [vps-miran-bots](vps-miran-bots.md) — VPS Миран, порты
|
||||
- [backup-howto](../backup/backup-howto.md) — бэкапы, расписание
|
||||
@@ -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 и сайт» со ссылкой на этот документ и указанием домена/порта.
|
||||
@@ -7,8 +7,8 @@ VPS в ЦОД Миран (Санкт-Петербург). Развёрнуты
|
||||
## Доступ и логины
|
||||
|
||||
- **SSH:** `ssh -p 15722 deploy@185.147.80.190` (пользователь deploy, в группе docker). IP: 185.147.80.190, хостнейм vm220416.vds.miran.ru, ОС Ubuntu.
|
||||
- **S3 (контент ботов):** URL https://api.s3.miran.ru, порт 443. Access key: `j3tears100@gmail.com`, Secret key: `wQ1-6sZEPs92sbZTSf96` (полная таблица — в разделе «S3» ниже).
|
||||
- **Админка Миран (панель хостинга VPS):** логин `j3tears100@gmail.com`, пароль `gonPok-xifrys-4nuxde`.
|
||||
- **S3 (контент ботов):** URL https://api.s3.miran.ru, порт 443. Access key и Secret key — в Vaultwarden (объект **MIRAN_S3**).
|
||||
- **Админка Миран (панель хостинга VPS):** логин и пароль — в Vaultwarden (отдельная запись для панели Миран).
|
||||
- **Grafana, Uptime Kuma, админки ботов:** логины и пароли — в `.env` проекта prod или в менеджере паролей.
|
||||
|
||||
---
|
||||
@@ -50,8 +50,8 @@ VPS в ЦОД Миран (Санкт-Петербург). Развёрнуты
|
||||
|-------------|----------|
|
||||
| URL | https://api.s3.miran.ru |
|
||||
| Порт | 443 (HTTPS) |
|
||||
| Access key | j3tears100@gmail.com |
|
||||
| Secret key | wQ1-6sZEPs92sbZTSf96 |
|
||||
| Access key | см. Vaultwarden, объект **MIRAN_S3** |
|
||||
| Secret key | см. Vaultwarden, объект **MIRAN_S3** |
|
||||
|
||||
В ботаx (переменные окружения prod) заданы `S3_ENDPOINT_URL=https://api.s3.miran.ru`, регион и креды для загрузки/выдачи контента. Для локальной разработки или других клиентов использовать те же endpoint и ключи.
|
||||
|
||||
@@ -81,6 +81,9 @@ VPS в ЦОД Миран (Санкт-Петербург). Развёрнуты
|
||||
| 9100 | node-exporter | TCP |
|
||||
| 3000 | Grafana | TCP |
|
||||
| 3001 | Uptime Kuma | TCP |
|
||||
| 8000 | Healthchecks (внутр.) | TCP |
|
||||
|
||||
**Healthchecks** — self-hosted Dead man's switch для homelab. Развёртывание: [healthchecks-miran-setup.md](healthchecks-miran-setup.md). Доступ через nginx (healthchecks.katykhin.ru).
|
||||
|
||||
---
|
||||
|
||||
@@ -110,7 +113,7 @@ docker compose logs -f telegram-bot
|
||||
|
||||
## Бэкап VPS (telegram-helper-bot)
|
||||
|
||||
Бэкап выполняется **с хоста Proxmox** скриптом `backup-vps-miran.sh` (cron 01:00). Копируются:
|
||||
Бэкап выполняется **с хоста Proxmox** скриптом `backup-vps-miran.sh` (systemd timer 01:00). Копируются:
|
||||
|
||||
1. **БД:** `/home/prod/bots/telegram-helper-bot/database/tg-bot-database.db` → `/mnt/backup/vps/miran/db/` (с датой в имени, хранение 14 дней).
|
||||
2. **Голосовые сообщения:** `/home/prod/bots/telegram-helper-bot/voice_users/` → `/mnt/backup/vps/miran/voice_users/` (rsync).
|
||||
@@ -119,12 +122,6 @@ docker compose logs -f telegram-bot
|
||||
**Что нужно на Proxmox:**
|
||||
|
||||
- **SSH:** с хоста (root) должен работать вход без пароля на `deploy@185.147.80.190 -p 15722` (добавить публичный ключ хоста в `~/.ssh/authorized_keys` пользователя deploy на VPS).
|
||||
- **S3:** установить `awscli` (`apt install awscli`) и создать файл `/root/.vps-miran-s3.env` с содержимым (подставить свои креды):
|
||||
```bash
|
||||
S3_ACCESS_KEY=j3tears100@gmail.com
|
||||
S3_SECRET_KEY=...
|
||||
S3_BUCKET_NAME=9829-telegram-helper-bot
|
||||
```
|
||||
Файл читается только root; в репозиторий не коммитить.
|
||||
- **S3:** установить `awscli` (`apt install awscli`). Креды S3 — в Vaultwarden (объект **MIRAN_S3**). Файл `/root/.vps-miran-s3.env` с `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_BUCKET_NAME` генерируется скриптами или создаётся вручную из Vaultwarden. Файл читается только root; в репозиторий не коммитить.
|
||||
|
||||
Подробности и восстановление — в [Бэкапы: как устроены и как восстанавливать](../backup/backup-howto.md).
|
||||
|
||||
@@ -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/vaultwarden-secrets.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/vaultwarden-secrets.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/vaultwarden-secrets.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
|
||||
|
||||
113
scripts/backup-restic-yandex-photos.sh
Normal file
113
scripts/backup-restic-yandex-photos.sh
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/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
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
export HOME="${HOME:-/root}"
|
||||
|
||||
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/vaultwarden-secrets.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,70 @@
|
||||
#!/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
|
||||
# При запуске из systemd PATH и HOME могут быть пустыми
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
export HOME="${HOME:-/root}"
|
||||
|
||||
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/vaultwarden-secrets.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 +72,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
|
||||
26
scripts/certbot-hooks/check-beget-credentials.sh
Normal file
26
scripts/certbot-hooks/check-beget-credentials.sh
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# Pre-hook для certbot: проверка beget.ini перед renew
|
||||
# Путь: /etc/letsencrypt/renewal-hooks/pre/check-beget-credentials.sh
|
||||
# При отсутствии файла или неверных правах — exit 1, certbot не выполнит renew.
|
||||
|
||||
BEGET_INI="/root/.secrets/certbot/beget.ini"
|
||||
|
||||
if [ ! -f "$BEGET_INI" ]; then
|
||||
echo "check-beget-credentials: $BEGET_INI not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mode=$(stat -c '%a' "$BEGET_INI" 2>/dev/null)
|
||||
owner=$(stat -c '%u' "$BEGET_INI" 2>/dev/null)
|
||||
|
||||
if [ "$mode" != "600" ]; then
|
||||
echo "check-beget-credentials: $BEGET_INI has mode $mode, expected 600" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$owner" != "0" ]; then
|
||||
echo "check-beget-credentials: $BEGET_INI owner $owner, expected root (0)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
28
scripts/dashboard/add-to-homepage.sh
Normal file
28
scripts/dashboard/add-to-homepage.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# Добавить Homelab Dashboard в Homepage (services.yaml на CT 103)
|
||||
# Запуск: с хоста Proxmox — pct exec 103 -- bash -s < /root/scripts/dashboard/add-to-homepage.sh
|
||||
|
||||
set -e
|
||||
|
||||
SERVICES_YAML="${SERVICES_YAML:-/opt/docker/homepage/config/services.yaml}"
|
||||
|
||||
if [ ! -f "$SERVICES_YAML" ]; then
|
||||
echo "ERROR: $SERVICES_YAML not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q "Homelab Dashboard" "$SERVICES_YAML" 2>/dev/null; then
|
||||
echo "Homelab Dashboard already in services.yaml"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Вставить после блока Netdata (ping: http://192.168.1.150:19999)
|
||||
sed -i '/ping: http:\/\/192.168.1.150:19999$/a\
|
||||
- Homelab Dashboard:\
|
||||
icon: mdi-chart-box\
|
||||
href: http://192.168.1.150:19998\
|
||||
description: Мониторинг хоста, контейнеров, сервисов\
|
||||
target: _blank
|
||||
' "$SERVICES_YAML"
|
||||
|
||||
echo "Added Homelab Dashboard to services.yaml"
|
||||
112
scripts/dashboard/dashboard-exporter.py
Normal file
112
scripts/dashboard/dashboard-exporter.py
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Экспортер метрик для дашборда homelab: disk % и OOM по контейнерам/VM.
|
||||
Запуск: python3 dashboard-exporter.py (выводит JSON в stdout)
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Маппинг: (vmid, type) -> (name, cgroup_name для Netdata)
|
||||
CONTAINERS = [
|
||||
(100, "lxc", "nginx", "cgroup_nginx"),
|
||||
(101, "lxc", "nextcloud", "cgroup_nextcloud"),
|
||||
(103, "lxc", "gitea", "cgroup_gitea"),
|
||||
(104, "lxc", "paperless", "cgroup_paperless"),
|
||||
(105, "lxc", "rag-service", "cgroup_rag-service"),
|
||||
(107, "lxc", "misc", "cgroup_misc"),
|
||||
(108, "lxc", "galene", "cgroup_galene"),
|
||||
(109, "lxc", "local-vpn", "cgroup_local-vpn"),
|
||||
(200, "qemu", "immich", "cgroup_qemu_immich"),
|
||||
]
|
||||
|
||||
LXC_CGROUP = Path("/sys/fs/cgroup/lxc")
|
||||
QEMU_CGROUP_200 = Path("/sys/fs/cgroup/qemu.slice/200.scope")
|
||||
|
||||
|
||||
def get_disk_pct_lxc(vmid: int) -> float | None:
|
||||
"""Disk % для LXC через pct exec df."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["pct", "exec", str(vmid), "--", "df", "-P", "/"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return None
|
||||
lines = r.stdout.strip().split("\n")
|
||||
if len(lines) < 2:
|
||||
return None
|
||||
# Формат: Filesystem 1K-blocks Used Available Use% Mounted
|
||||
parts = lines[-1].split()
|
||||
if len(parts) >= 5:
|
||||
use_pct = parts[4].rstrip("%")
|
||||
return float(use_pct)
|
||||
except (subprocess.TimeoutExpired, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def get_disk_pct_vm200() -> float | None:
|
||||
"""Disk % для VM 200 через lvs (fallback, т.к. qm guest exec часто недоступен)."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["lvs", "-o", "data_percent", "--noheadings", "pve/vm-200-disk-0"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return None
|
||||
val = r.stdout.strip()
|
||||
if val:
|
||||
return float(val)
|
||||
except (subprocess.TimeoutExpired, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def get_oom_count(vmid: int, vmtype: str) -> int | None:
|
||||
"""OOM count из cgroup memory.events."""
|
||||
if vmtype == "lxc":
|
||||
path = LXC_CGROUP / str(vmid) / "memory.events"
|
||||
elif vmtype == "qemu" and vmid == 200:
|
||||
path = QEMU_CGROUP_200 / "memory.events"
|
||||
else:
|
||||
return None
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
text = path.read_text()
|
||||
for line in text.splitlines():
|
||||
if line.startswith("oom_kill "):
|
||||
return int(line.split()[1])
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
result = {"containers": [], "ok": True}
|
||||
for vmid, vmtype, name, cgroup_name in CONTAINERS:
|
||||
disk_pct = None
|
||||
if vmtype == "lxc":
|
||||
disk_pct = get_disk_pct_lxc(vmid)
|
||||
elif vmtype == "qemu" and vmid == 200:
|
||||
disk_pct = get_disk_pct_vm200()
|
||||
oom = get_oom_count(vmid, vmtype)
|
||||
result["containers"].append({
|
||||
"vmid": vmid,
|
||||
"name": name,
|
||||
"cgroup_name": cgroup_name,
|
||||
"disk_pct": disk_pct,
|
||||
"oom_count": oom,
|
||||
})
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
104
scripts/dashboard/dashboard-server.py
Normal file
104
scripts/dashboard/dashboard-server.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP-сервер дашборда homelab: статика, /api/containers, прокси к Netdata.
|
||||
Порт: 19998 (по умолчанию).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from pathlib import Path
|
||||
|
||||
PORT = int(os.environ.get("DASHBOARD_PORT", "19998"))
|
||||
NETDATA_URL = os.environ.get("NETDATA_URL", "http://127.0.0.1:19999")
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
EXPORTER = SCRIPT_DIR / "dashboard-exporter.py"
|
||||
|
||||
|
||||
class DashboardHandler(BaseHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
pass # подавить вывод в консоль
|
||||
|
||||
def send_json(self, data: dict, status: int = 200):
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
|
||||
|
||||
def send_html(self, html: bytes, status: int = 200):
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.end_headers()
|
||||
self.wfile.write(html)
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?")[0].rstrip("/") or "/"
|
||||
if path == "/":
|
||||
self.serve_index()
|
||||
elif path == "/api/containers":
|
||||
self.serve_containers()
|
||||
elif path.startswith("/api/netdata"):
|
||||
self.proxy_netdata()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def serve_index(self):
|
||||
html_file = SCRIPT_DIR / "index.html"
|
||||
if html_file.exists():
|
||||
self.send_html(html_file.read_bytes())
|
||||
else:
|
||||
self.send_error(404, "index.html not found")
|
||||
|
||||
def serve_containers(self):
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(EXPORTER)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
cwd=str(SCRIPT_DIR),
|
||||
)
|
||||
if r.returncode != 0:
|
||||
self.send_json({"ok": False, "error": r.stderr or "exporter failed"}, 500)
|
||||
return
|
||||
data = json.loads(r.stdout)
|
||||
self.send_json(data)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.send_json({"ok": False, "error": "timeout"}, 504)
|
||||
except json.JSONDecodeError as e:
|
||||
self.send_json({"ok": False, "error": str(e)}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({"ok": False, "error": str(e)}, 500)
|
||||
|
||||
def proxy_netdata(self):
|
||||
qs = self.path.split("?", 1)[1] if "?" in self.path else ""
|
||||
url = f"{NETDATA_URL}/api/v1/data?{qs}" if qs else f"{NETDATA_URL}/api/v1/data"
|
||||
try:
|
||||
req = urllib.request.Request(url)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = resp.read()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", resp.headers.get("Content-Type", "application/json"))
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
except Exception as e:
|
||||
self.send_json({"error": str(e)}, 502)
|
||||
|
||||
|
||||
def main():
|
||||
server = HTTPServer(("0.0.0.0", PORT), DashboardHandler)
|
||||
print(f"Dashboard server on http://0.0.0.0:{PORT}", file=sys.stderr)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
35
scripts/dashboard/deploy-dashboard.sh
Normal file
35
scripts/dashboard/deploy-dashboard.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Деплой дашборда homelab на хост Proxmox
|
||||
# Запуск: с хоста Proxmox или ssh root@192.168.1.150 'bash -s' < scripts/dashboard/deploy-dashboard.sh
|
||||
# Или из репозитория: ./scripts/dashboard/deploy-dashboard.sh (копирует из текущей директории)
|
||||
|
||||
set -e
|
||||
|
||||
# REPO_ROOT: корень репозитория (содержит scripts/dashboard/)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="${REPO_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
||||
DASHBOARD_SRC="${REPO_ROOT}/scripts/dashboard"
|
||||
DEST="/root/scripts/dashboard"
|
||||
SYSTEMD_DEST="/etc/systemd/system"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
|
||||
log "Deploying homelab dashboard..."
|
||||
|
||||
mkdir -p "$DEST"
|
||||
if [ "$(realpath "$DASHBOARD_SRC")" != "$(realpath "$DEST")" ]; then
|
||||
cp -v "${DASHBOARD_SRC}/dashboard-exporter.py" "$DEST/"
|
||||
cp -v "${DASHBOARD_SRC}/dashboard-server.py" "$DEST/"
|
||||
cp -v "${DASHBOARD_SRC}/index.html" "$DEST/"
|
||||
fi
|
||||
chmod +x "${DEST}/dashboard-exporter.py" "${DEST}/dashboard-server.py"
|
||||
|
||||
if [ -f "${REPO_ROOT}/scripts/systemd/homelab-dashboard.service" ]; then
|
||||
cp -v "${REPO_ROOT}/scripts/systemd/homelab-dashboard.service" "$SYSTEMD_DEST/"
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
systemctl enable homelab-dashboard.service
|
||||
systemctl restart homelab-dashboard.service
|
||||
|
||||
log "Dashboard deployed. URL: http://192.168.1.150:19998"
|
||||
log "Status: $(systemctl is-active homelab-dashboard.service)"
|
||||
210
scripts/dashboard/index.html
Normal file
210
scripts/dashboard/index.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Homelab Dashboard</title>
|
||||
<style>
|
||||
:root { --bg: #0d1117; --card: #161b22; --text: #e6edf3; --muted: #8b949e; --accent: #58a6ff; --ok: #3fb950; --warn: #d29922; --err: #f85149; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 1rem; line-height: 1.5; }
|
||||
h1 { font-size: 1.25rem; margin: 0 0 1rem; }
|
||||
h2 { font-size: 1rem; margin: 0 0 0.5rem; color: var(--muted); font-weight: 500; }
|
||||
.card { background: var(--card); border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0.75rem; }
|
||||
.metric { text-align: center; }
|
||||
.metric-value { font-size: 1.5rem; font-weight: 600; }
|
||||
.metric-label { font-size: 0.75rem; color: var(--muted); }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #30363d; }
|
||||
th { color: var(--muted); font-weight: 500; font-size: 0.85rem; }
|
||||
.pct-ok { color: var(--ok); }
|
||||
.pct-warn { color: var(--warn); }
|
||||
.pct-err { color: var(--err); }
|
||||
.links { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
|
||||
.links a { color: var(--accent); text-decoration: none; font-size: 0.9rem; }
|
||||
.links a:hover { text-decoration: underline; }
|
||||
.loading { color: var(--muted); }
|
||||
.error { color: var(--err); }
|
||||
.updated { font-size: 0.75rem; color: var(--muted); margin-top: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Homelab Dashboard</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2>Блок 1 — Хост</h2>
|
||||
<div class="grid" id="host-metrics">
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">CPU %</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">RAM</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Load</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">iowait %</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Disk /</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Disk backup</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Disk nextcloud-hdd</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Disk tank</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Блок 2 — Контейнеры</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Контейнер</th><th>CPU %</th><th>RAM %</th><th>Disk %</th><th>OOM</th></tr>
|
||||
</thead>
|
||||
<tbody id="containers-table"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Блок 3 — Критические сервисы</h2>
|
||||
<div class="links">
|
||||
<a href="http://192.168.1.150:19999/#menu_system_submenu_cpu;netdata" target="_blank">Netdata (CPU)</a>
|
||||
<a href="http://192.168.1.150:19999/#menu_cgroup_nginx;netdata" target="_blank">nginx (CT 100)</a>
|
||||
<a href="http://192.168.1.150:19999/#menu_cgroup_nextcloud;netdata" target="_blank">Nextcloud (CT 101)</a>
|
||||
<a href="http://192.168.1.150:19999/#menu_cgroup_qemu_immich;netdata" target="_blank">Immich (VM 200)</a>
|
||||
<a href="http://192.168.1.150:19999/#menu_cgroup_local-vpn;netdata" target="_blank">VPN (CT 109)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="updated" id="updated"></div>
|
||||
<div id="status" class="updated" style="color:var(--muted)"></div>
|
||||
|
||||
<script>
|
||||
const API = window.location.origin; // явно использовать текущий origin
|
||||
|
||||
function pctClass(v) {
|
||||
if (v == null) return '';
|
||||
if (v >= 90) return 'pct-err';
|
||||
if (v >= 75) return 'pct-warn';
|
||||
return 'pct-ok';
|
||||
}
|
||||
|
||||
function fmt(v, suffix = '') {
|
||||
if (v == null || v === undefined) return '—';
|
||||
if (typeof v === 'number') return v.toFixed(1) + suffix;
|
||||
return String(v) + suffix;
|
||||
}
|
||||
|
||||
async function fetchNetdata(chart, points = 1) {
|
||||
const url = `${API}/api/netdata?chart=${encodeURIComponent(chart)}&points=${points}&format=json`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`${chart}: ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async function loadHost() {
|
||||
try {
|
||||
const results = await Promise.allSettled([
|
||||
fetchNetdata('system.cpu'),
|
||||
fetchNetdata('system.ram'),
|
||||
fetchNetdata('system.load'),
|
||||
fetchNetdata('disk_space./'),
|
||||
fetchNetdata('disk_space./mnt/backup'),
|
||||
fetchNetdata('disk_space./mnt/nextcloud-hdd'),
|
||||
fetchNetdata('disk_space./tank'),
|
||||
]);
|
||||
const [cpu, ram, load, diskRoot, diskBackup, diskNextcloud, diskTank] = results.map(r => r.status === 'fulfilled' ? r.value : null);
|
||||
const cpuData = cpu?.data?.[0];
|
||||
const ramData = ram?.data?.[0];
|
||||
const loadData = load?.data?.[0];
|
||||
const li = cpu?.labels || [];
|
||||
const cpuTotal = cpuData ? (li.indexOf('user') >= 0 ? (cpuData[li.indexOf('user')] || 0) + (cpuData[li.indexOf('system')] || 0) + (cpuData[li.indexOf('nice')] || 0) + (cpuData[li.indexOf('iowait')] || 0) + (cpuData[li.indexOf('irq')] || 0) + (cpuData[li.indexOf('softirq')] || 0) + (cpuData[li.indexOf('steal')] || 0) + (cpuData[li.indexOf('guest')] || 0) + (cpuData[li.indexOf('guest_nice')] || 0) : 0) : null;
|
||||
const iowait = cpuData && li.indexOf('iowait') >= 0 ? cpuData[li.indexOf('iowait')] : null;
|
||||
const ramUsed = ramData && ram?.labels ? ramData[ram.labels.indexOf('used')] : null;
|
||||
const load15 = loadData && load?.labels ? loadData[load.labels.indexOf('load15')] : null;
|
||||
// disk_space возвращает avail/used в GiB, считаем %: used/(used+avail)*100
|
||||
const diskPct = (d) => {
|
||||
if (!d?.data?.[0] || !d?.labels) return null;
|
||||
const idxU = d.labels.indexOf('used'), idxA = d.labels.indexOf('avail');
|
||||
if (idxU < 0 || idxA < 0) return null;
|
||||
const used = d.data[0][idxU], avail = d.data[0][idxA];
|
||||
const total = used + avail;
|
||||
return total > 0 ? (used / total * 100) : null;
|
||||
};
|
||||
const diskRootUsed = diskPct(diskRoot);
|
||||
const diskBackupUsed = diskPct(diskBackup);
|
||||
const diskNextcloudUsed = diskPct(diskNextcloud);
|
||||
const diskTankUsed = diskPct(diskTank);
|
||||
|
||||
document.getElementById('host-metrics').innerHTML = `
|
||||
<div class="metric"><span class="metric-value">${fmt(cpuTotal, '%')}</span><span class="metric-label">CPU %</span></div>
|
||||
<div class="metric"><span class="metric-value">${fmt(ramUsed, ' MiB')}</span><span class="metric-label">RAM used</span></div>
|
||||
<div class="metric"><span class="metric-value">${fmt(load15)}</span><span class="metric-label">Load 15</span></div>
|
||||
<div class="metric"><span class="metric-value">${fmt(iowait, '%')}</span><span class="metric-label">iowait %</span></div>
|
||||
<div class="metric"><span class="metric-value ${pctClass(diskRootUsed)}">${fmt(diskRootUsed, '%')}</span><span class="metric-label">Disk /</span></div>
|
||||
<div class="metric"><span class="metric-value ${pctClass(diskBackupUsed)}">${fmt(diskBackupUsed, '%')}</span><span class="metric-label">Disk backup</span></div>
|
||||
<div class="metric"><span class="metric-value ${pctClass(diskNextcloudUsed)}">${fmt(diskNextcloudUsed, '%')}</span><span class="metric-label">Disk nextcloud-hdd</span></div>
|
||||
<div class="metric"><span class="metric-value ${pctClass(diskTankUsed)}">${fmt(diskTankUsed, '%')}</span><span class="metric-label">Disk tank</span></div>
|
||||
`;
|
||||
} catch (e) {
|
||||
document.getElementById('host-metrics').innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
const CGROUP_CHARTS = {
|
||||
'cgroup_nginx': { cpu: 'cgroup_nginx.cpu_limit', mem: 'cgroup_nginx.mem_utilization' },
|
||||
'cgroup_nextcloud': { cpu: 'cgroup_nextcloud.cpu_limit', mem: 'cgroup_nextcloud.mem_utilization' },
|
||||
'cgroup_gitea': { cpu: 'cgroup_gitea.cpu_limit', mem: 'cgroup_gitea.mem_utilization' },
|
||||
'cgroup_paperless': { cpu: 'cgroup_paperless.cpu_limit', mem: 'cgroup_paperless.mem_utilization' },
|
||||
'cgroup_rag-service': { cpu: 'cgroup_rag-service.cpu_limit', mem: 'cgroup_rag-service.mem_utilization' },
|
||||
'cgroup_misc': { cpu: 'cgroup_misc.cpu_limit', mem: 'cgroup_misc.mem_utilization' },
|
||||
'cgroup_galene': { cpu: 'cgroup_galene.cpu_limit', mem: 'cgroup_galene.mem_utilization' },
|
||||
'cgroup_local-vpn': { cpu: 'cgroup_local-vpn.cpu_limit', mem: 'cgroup_local-vpn.mem_utilization' },
|
||||
'cgroup_qemu_immich': { cpu: 'cgroup_qemu_immich.cpu_limit', mem: 'cgroup_qemu_immich.mem_utilization' },
|
||||
};
|
||||
|
||||
async function loadContainers() {
|
||||
try {
|
||||
const containersRes = await fetch(`${API}/api/containers`);
|
||||
if (!containersRes.ok) throw new Error(`API ${containersRes.status}`);
|
||||
const containersData = await containersRes.json();
|
||||
if (!containersData.ok || !containersData.containers) {
|
||||
document.getElementById('containers-table').innerHTML = `<tr><td colspan="5" class="error">Ошибка загрузки</td></tr>`;
|
||||
return;
|
||||
}
|
||||
const containers = containersData.containers;
|
||||
const cpuPromises = containers.map(c => {
|
||||
const charts = CGROUP_CHARTS[c.cgroup_name];
|
||||
if (!charts) return [null, null];
|
||||
return Promise.all([
|
||||
fetchNetdata(charts.cpu).then(d => d.data?.[0]?.[d.labels?.indexOf('used') ?? 0] != null ? d.data[0][d.labels.indexOf('used')] * 100 : null),
|
||||
fetchNetdata(charts.mem).then(d => d.data?.[0]?.[d.labels?.indexOf('utilization') ?? 0] != null ? d.data[0][d.labels.indexOf('utilization')] : null),
|
||||
]);
|
||||
});
|
||||
const netdataRows = await Promise.all(cpuPromises);
|
||||
const rows = containers.map((c, i) => {
|
||||
const [cpuPct, ramPct] = netdataRows[i] || [null, null];
|
||||
return `<tr>
|
||||
<td>${c.name} (${c.vmid})</td>
|
||||
<td class="${pctClass(cpuPct)}">${fmt(cpuPct, '%')}</td>
|
||||
<td class="${pctClass(ramPct)}">${fmt(ramPct, '%')}</td>
|
||||
<td class="${pctClass(c.disk_pct)}">${fmt(c.disk_pct, '%')}</td>
|
||||
<td>${c.oom_count != null ? c.oom_count : '—'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
document.getElementById('containers-table').innerHTML = rows.join('');
|
||||
} catch (e) {
|
||||
document.getElementById('containers-table').innerHTML = `<tr><td colspan="5" class="error">Ошибка: ${e.message}. Проверьте доступ к ${API}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const statusEl = document.getElementById('status');
|
||||
statusEl.textContent = 'Загрузка...';
|
||||
try {
|
||||
await Promise.all([loadHost(), loadContainers()]);
|
||||
document.getElementById('updated').textContent = 'Обновлено: ' + new Date().toLocaleString('ru');
|
||||
statusEl.textContent = '';
|
||||
} catch (e) {
|
||||
document.getElementById('updated').textContent = '';
|
||||
statusEl.textContent = 'Ошибка: ' + e.message;
|
||||
statusEl.style.color = 'var(--err)';
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
116
scripts/deploy-beget-credentials.sh
Normal file
116
scripts/deploy-beget-credentials.sh
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/bin/bash
|
||||
# deploy-beget-credentials.sh — деплой кредов Beget для certbot DNS-01 в CT 100
|
||||
# Секреты из Vaultwarden (объект beget). Атомарная запись beget.ini.
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-beget-credentials.sh # деплой
|
||||
# /root/scripts/deploy-beget-credentials.sh --dry-run # проверка без записи
|
||||
#
|
||||
# Ротация: сменил пароль в Vaultwarden → запустил скрипт → готово.
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master (chmod 600)
|
||||
|
||||
set -e
|
||||
|
||||
CT_ID=100
|
||||
BEGET_INI_PATH="/root/.secrets/certbot/beget.ini"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
get_secrets() {
|
||||
BEGET_USER=$(bw get username "beget" 2>/dev/null)
|
||||
BEGET_PASS=$(bw get password "beget" 2>/dev/null)
|
||||
if [ -z "$BEGET_USER" ] || [ -z "$BEGET_PASS" ]; then
|
||||
err "beget: missing username or password in Vaultwarden"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
gen_ini() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp" << EOF
|
||||
dns_beget_api_username = ${BEGET_USER}
|
||||
dns_beget_api_password = ${BEGET_PASS}
|
||||
EOF
|
||||
echo "$tmp"
|
||||
}
|
||||
|
||||
push_ini_atomic() {
|
||||
local tmp="$1"
|
||||
local dir
|
||||
dir=$(dirname "$BEGET_INI_PATH")
|
||||
pct exec "$CT_ID" -- mkdir -p "$dir"
|
||||
pct push "$CT_ID" "$tmp" "${BEGET_INI_PATH}.tmp"
|
||||
pct exec "$CT_ID" -- bash -c "mv ${BEGET_INI_PATH}.tmp ${BEGET_INI_PATH} && chmod 600 ${BEGET_INI_PATH} && chown root:root ${BEGET_INI_PATH}"
|
||||
log "beget.ini written (atomic), chmod 600, owner root"
|
||||
}
|
||||
|
||||
deploy_pre_hook() {
|
||||
local hook_path="/etc/letsencrypt/renewal-hooks/pre/check-beget-credentials.sh"
|
||||
local hook_src
|
||||
hook_src="$(cd "$(dirname "$0")" && pwd)/certbot-hooks/check-beget-credentials.sh"
|
||||
if [ ! -f "$hook_src" ]; then
|
||||
log "pre-hook source not found ($hook_src), skip"
|
||||
return 0
|
||||
fi
|
||||
if pct exec "$CT_ID" -- test -f "$hook_path" 2>/dev/null; then
|
||||
pct push "$CT_ID" "$hook_src" "$hook_path"
|
||||
pct exec "$CT_ID" -- chmod +x "$hook_path"
|
||||
log "pre-hook updated"
|
||||
else
|
||||
pct push "$CT_ID" "$hook_src" "$hook_path"
|
||||
pct exec "$CT_ID" -- chmod +x "$hook_path"
|
||||
log "pre-hook deployed"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
log "deploy-beget-credentials start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push beget.ini and deploy pre-hook"
|
||||
log " dns_beget_api_username=$BEGET_USER"
|
||||
log " dns_beget_api_password=***"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=$(gen_ini)
|
||||
trap "rm -f $tmp" EXIT
|
||||
push_ini_atomic "$tmp"
|
||||
deploy_pre_hook
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
94
scripts/deploy-galene-credentials.sh
Normal file
94
scripts/deploy-galene-credentials.sh
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
# deploy-galene-credentials.sh — деплой TURN-кредов Galene в CT 108
|
||||
# Секреты из Vaultwarden (объект GALENE, поле config — JSON ice-servers).
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-galene-credentials.sh
|
||||
# /root/scripts/deploy-galene-credentials.sh --dry-run
|
||||
#
|
||||
# Ротация: сменил TURN username/credential в Vaultwarden → запустил скрипт → systemctl restart galene
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master (chmod 600)
|
||||
|
||||
set -e
|
||||
|
||||
CT_ID=108
|
||||
ICE_SERVERS_PATH="/opt/galene-data/data/ice-servers.json"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
get_secrets() {
|
||||
local config
|
||||
config=$(bw get item "GALENE" 2>/dev/null | jq -r '.fields[] | select(.name=="config") | .value // empty')
|
||||
if [ -z "$config" ]; then
|
||||
err "GALENE: missing config field (JSON ice-servers)"
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$config" | jq . >/dev/null 2>&1; then
|
||||
err "GALENE config: invalid JSON"
|
||||
exit 1
|
||||
fi
|
||||
ICE_CONFIG="$config"
|
||||
}
|
||||
|
||||
push_ice_servers() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
echo "$ICE_CONFIG" | jq -c . > "$tmp"
|
||||
pct push "$CT_ID" "$tmp" "${ICE_SERVERS_PATH}.tmp"
|
||||
rm -f "$tmp"
|
||||
pct exec "$CT_ID" -- bash -c "chmod 600 ${ICE_SERVERS_PATH}.tmp && mv ${ICE_SERVERS_PATH}.tmp ${ICE_SERVERS_PATH}"
|
||||
log "ice-servers.json written (atomic), chmod 600"
|
||||
}
|
||||
|
||||
restart_galene() {
|
||||
pct exec "$CT_ID" -- systemctl restart galene
|
||||
log "galene restarted"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "deploy-galene-credentials start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push ice-servers.json and restart galene"
|
||||
log " config: $(echo "$ICE_CONFIG" | jq -c .)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
push_ice_servers
|
||||
restart_galene
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
117
scripts/deploy-gitea-credentials.sh
Normal file
117
scripts/deploy-gitea-credentials.sh
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# deploy-gitea-credentials.sh — деплой кредов Gitea в CT 103
|
||||
# Секреты из Vaultwarden (объект GITEA). Атомарная запись .env.
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-gitea-credentials.sh
|
||||
# /root/scripts/deploy-gitea-credentials.sh --dry-run
|
||||
#
|
||||
# Ротация: сменил пароль/токен в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master (chmod 600)
|
||||
|
||||
set -e
|
||||
|
||||
CT_ID=103
|
||||
GITEA_PATH="/opt/gitea"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
get_secrets() {
|
||||
local item
|
||||
item=$(bw get item "GITEA" 2>/dev/null)
|
||||
POSTGRES_PASSWORD=$(bw get password "GITEA" 2>/dev/null)
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN=$(echo "$item" | jq -r '.fields[] | select(.name=="GITEA_RUNNER_REGISTRATION_TOKEN") | .value // empty')
|
||||
|
||||
if [ -z "$POSTGRES_PASSWORD" ]; then
|
||||
err "GITEA: missing password (POSTGRES_PASSWORD)"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$GITEA_RUNNER_REGISTRATION_TOKEN" ]; then
|
||||
err "GITEA: missing GITEA_RUNNER_REGISTRATION_TOKEN field"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
gen_env() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp" << EOF
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN}
|
||||
EOF
|
||||
echo "$tmp"
|
||||
}
|
||||
|
||||
push_env_atomic() {
|
||||
local tmp="$1"
|
||||
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${GITEA_PATH}/.env.tmp && chmod 600 ${GITEA_PATH}/.env.tmp && mv ${GITEA_PATH}/.env.tmp ${GITEA_PATH}/.env"
|
||||
log ".env written (atomic), chmod 600"
|
||||
}
|
||||
|
||||
push_compose() {
|
||||
local compose_src="${SCRIPT_DIR}/gitea/docker-compose.yml"
|
||||
if [ -f "$compose_src" ]; then
|
||||
pct push "$CT_ID" "$compose_src" "${GITEA_PATH}/docker-compose.yml"
|
||||
log "docker-compose.yml pushed"
|
||||
else
|
||||
log "WARN: ${compose_src} not found, skipping compose push"
|
||||
fi
|
||||
}
|
||||
|
||||
run_compose() {
|
||||
pct exec "$CT_ID" -- bash -c "cd ${GITEA_PATH} && docker compose up -d --force-recreate"
|
||||
log "Gitea started"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "deploy-gitea-credentials start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push .env and run compose"
|
||||
log " POSTGRES_PASSWORD=***"
|
||||
log " GITEA_RUNNER_REGISTRATION_TOKEN=***"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=$(gen_env)
|
||||
trap "rm -f $tmp" EXIT
|
||||
push_env_atomic "$tmp"
|
||||
push_compose
|
||||
run_compose
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
176
scripts/deploy-immich-credentials.sh
Normal file
176
scripts/deploy-immich-credentials.sh
Normal file
@@ -0,0 +1,176 @@
|
||||
#!/bin/bash
|
||||
# deploy-immich-credentials.sh — деплой кредов Immich и immich-deduper на VM 200
|
||||
# Секреты из Vaultwarden (объекты IMMICH, IMMICH_DEDUPER).
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-immich-credentials.sh
|
||||
# /root/scripts/deploy-immich-credentials.sh --dry-run
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master, SSH без пароля root@host → admin@192.168.1.200
|
||||
#
|
||||
# Vaultwarden: IMMICH — поля DB_PASSWORD, IMMICH_API_KEY, GEMINI_API_KEY и др. (см. .env).
|
||||
# IMMICH_DEDUPER — поля PSQL_PASS, DEDUP_*, IMMICH_PATH, PSQL_*.
|
||||
|
||||
set -e
|
||||
|
||||
VM_SSH="admin@192.168.1.200"
|
||||
IMMICH_PATH="/opt/immich"
|
||||
DEDUPER_PATH="/opt/immich-deduper"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
get_field() {
|
||||
local item="$1" name="$2"
|
||||
echo "$item" | jq -r ".fields[] | select(.name==\"$name\") | .value // empty"
|
||||
}
|
||||
|
||||
get_immich_secrets() {
|
||||
local id
|
||||
id=$(bw list items --search IMMICH 2>/dev/null | jq -r '.[] | select(.name=="IMMICH") | .id' | head -1) || true
|
||||
[ -z "$id" ] && id=$(bw list items 2>/dev/null | jq -r '.[] | select(.name=="IMMICH") | .id' | head -1) || true
|
||||
[ -z "$id" ] && { err "IMMICH not found in Vaultwarden"; exit 1; }
|
||||
IMMICH_ITEM=$(bw get item "$id" 2>/dev/null) || { err "IMMICH get item failed for id=$id"; exit 1; }
|
||||
DB_PASSWORD=$(get_field "$IMMICH_ITEM" "DB_PASSWORD")
|
||||
IMMICH_API_KEY=$(get_field "$IMMICH_ITEM" "IMMICH_API_KEY")
|
||||
GEMINI_API_KEY=$(get_field "$IMMICH_ITEM" "GEMINI_API_KEY")
|
||||
if [ -z "$DB_PASSWORD" ]; then err "IMMICH: missing DB_PASSWORD field"; exit 1; fi
|
||||
if [ -z "$IMMICH_API_KEY" ]; then err "IMMICH: missing IMMICH_API_KEY field"; exit 1; fi
|
||||
}
|
||||
|
||||
get_deduper_secrets() {
|
||||
local id
|
||||
id=$(bw list items 2>/dev/null | jq -r '.[] | select(.name=="IMMICH_DEDUPER") | .id' | head -1)
|
||||
[ -z "$id" ] && { err "IMMICH_DEDUPER not found in Vaultwarden"; exit 1; }
|
||||
DEDUP_ITEM=$(bw get item "$id" 2>/dev/null) || {
|
||||
err "IMMICH_DEDUPER not found in Vaultwarden"
|
||||
exit 1
|
||||
}
|
||||
PSQL_PASS=$(get_field "$DEDUP_ITEM" "PSQL_PASS")
|
||||
[ -z "$PSQL_PASS" ] && PSQL_PASS=$(echo "$DEDUP_ITEM" | jq -r '.login.password // empty')
|
||||
DEDUP_PORT=$(get_field "$DEDUP_ITEM" "DEDUP_PORT")
|
||||
DEDUP_DATA=$(get_field "$DEDUP_ITEM" "DEDUP_DATA")
|
||||
DEDUP_IMAGE=$(get_field "$DEDUP_ITEM" "DEDUP_IMAGE")
|
||||
IMMICH_PATH_FIELD=$(get_field "$DEDUP_ITEM" "IMMICH_PATH")
|
||||
PSQL_HOST=$(get_field "$DEDUP_ITEM" "PSQL_HOST")
|
||||
PSQL_PORT=$(get_field "$DEDUP_ITEM" "PSQL_PORT")
|
||||
PSQL_DB=$(get_field "$DEDUP_ITEM" "PSQL_DB")
|
||||
[ -z "$PSQL_PASS" ] && PSQL_PASS="${DB_PASSWORD:-}"
|
||||
DEDUP_PORT="${DEDUP_PORT:-8086}"
|
||||
DEDUP_DATA="${DEDUP_DATA:-/opt/immich-deduper/data}"
|
||||
DEDUP_IMAGE="${DEDUP_IMAGE:-razgrizhsu/immich-deduper:latest-cpu}"
|
||||
IMMICH_PATH_FIELD="${IMMICH_PATH_FIELD:-/mnt/data/library}"
|
||||
PSQL_HOST="${PSQL_HOST:-database}"
|
||||
PSQL_PORT="${PSQL_PORT:-5432}"
|
||||
PSQL_DB="${PSQL_DB:-immich}"
|
||||
}
|
||||
|
||||
gen_immich_env() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp" << EOF
|
||||
# Immich .env (generated from Vaultwarden)
|
||||
UPLOAD_LOCATION=/mnt/data/library
|
||||
DB_DATA_LOCATION=/mnt/data/postgres
|
||||
IMMICH_VERSION=v2
|
||||
DB_PASSWORD=${DB_PASSWORD}
|
||||
DB_USERNAME=postgres
|
||||
DB_DATABASE_NAME=immich
|
||||
IMMICH_URL=http://immich-server:2283
|
||||
IMMICH_API_KEY=${IMMICH_API_KEY}
|
||||
DB_HOST=immich_postgres
|
||||
DB_PORT=5432
|
||||
EXTERNAL_IMMICH_URL=https://immich.katykhin.ru
|
||||
GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
EOF
|
||||
echo "$tmp"
|
||||
}
|
||||
|
||||
gen_deduper_env() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp" << EOF
|
||||
# Deduper .env (generated from Vaultwarden)
|
||||
DEDUP_PORT=${DEDUP_PORT}
|
||||
DEDUP_DATA=${DEDUP_DATA}
|
||||
DEDUP_IMAGE=${DEDUP_IMAGE}
|
||||
IMMICH_PATH=${IMMICH_PATH_FIELD}
|
||||
PSQL_HOST=${PSQL_HOST}
|
||||
PSQL_PORT=${PSQL_PORT}
|
||||
PSQL_DB=${PSQL_DB}
|
||||
PSQL_USER=postgres
|
||||
PSQL_PASS=${PSQL_PASS}
|
||||
EOF
|
||||
echo "$tmp"
|
||||
}
|
||||
|
||||
push_to_vm() {
|
||||
local local_file="$1" remote_path="$2"
|
||||
scp -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -q "$local_file" "${VM_SSH}:/tmp/deploy-env.tmp" || {
|
||||
err "scp to ${VM_SSH} failed. Ensure SSH key from Proxmox: ssh-copy-id ${VM_SSH}"
|
||||
exit 1
|
||||
}
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "$VM_SSH" "sudo mv /tmp/deploy-env.tmp ${remote_path} && sudo chmod 600 ${remote_path}" || {
|
||||
err "ssh to ${VM_SSH} failed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
run_compose() {
|
||||
ssh -o BatchMode=yes "$VM_SSH" "cd ${IMMICH_PATH} && sudo docker compose up -d --force-recreate"
|
||||
ssh -o BatchMode=yes "$VM_SSH" "cd ${DEDUPER_PATH} && sudo docker compose up -d --force-recreate"
|
||||
log "Immich and deduper started"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "deploy-immich-credentials start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_immich_secrets
|
||||
get_deduper_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push .env files and run compose"
|
||||
log " DB_PASSWORD=*** IMMICH_API_KEY=***"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp_immich=$(gen_immich_env)
|
||||
tmp_deduper=$(gen_deduper_env)
|
||||
trap "rm -f $tmp_immich $tmp_deduper" EXIT
|
||||
push_to_vm "$tmp_immich" "${IMMICH_PATH}/.env"
|
||||
log "Immich .env written"
|
||||
push_to_vm "$tmp_deduper" "${DEDUPER_PATH}/.env"
|
||||
log "Deduper .env written"
|
||||
run_compose
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
116
scripts/deploy-invidious-credentials.sh
Normal file
116
scripts/deploy-invidious-credentials.sh
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/bin/bash
|
||||
# deploy-invidious-credentials.sh — деплой кредов Invidious в CT 107
|
||||
# Секреты из Vaultwarden (объект INVIDIOUS). Атомарная запись .env.
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-invidious-credentials.sh
|
||||
# /root/scripts/deploy-invidious-credentials.sh --dry-run
|
||||
#
|
||||
# Ротация: сменил пароль/ключи в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master (chmod 600)
|
||||
|
||||
set -e
|
||||
|
||||
CT_ID=107
|
||||
INVIDIOUS_PATH="/opt/invidious"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
get_secrets() {
|
||||
local item
|
||||
item=$(bw get item "INVIDIOUS" 2>/dev/null)
|
||||
POSTGRES_USER=$(echo "$item" | jq -r '.login.username // empty')
|
||||
POSTGRES_PASSWORD=$(bw get password "INVIDIOUS" 2>/dev/null)
|
||||
INVIDIOUS_COMPANION_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="SERVER_SECRET_KEY") | .value // empty')
|
||||
HMAC_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="HMAC_KEY") | .value // empty')
|
||||
|
||||
if [ -z "$POSTGRES_USER" ] || [ -z "$POSTGRES_PASSWORD" ]; then
|
||||
err "INVIDIOUS: missing username or password"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$INVIDIOUS_COMPANION_KEY" ]; then
|
||||
err "INVIDIOUS: missing SERVER_SECRET_KEY field"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$HMAC_KEY" ]; then
|
||||
err "INVIDIOUS: missing HMAC_KEY field"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
gen_env() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp" << EOF
|
||||
POSTGRES_USER=${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB=invidious
|
||||
INVIDIOUS_COMPANION_KEY=${INVIDIOUS_COMPANION_KEY}
|
||||
HMAC_KEY=${HMAC_KEY}
|
||||
EOF
|
||||
echo "$tmp"
|
||||
}
|
||||
|
||||
push_env_atomic() {
|
||||
local tmp="$1"
|
||||
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${INVIDIOUS_PATH}/.env.tmp && chmod 600 ${INVIDIOUS_PATH}/.env.tmp && mv ${INVIDIOUS_PATH}/.env.tmp ${INVIDIOUS_PATH}/.env"
|
||||
log ".env written (atomic), chmod 600"
|
||||
}
|
||||
|
||||
run_compose() {
|
||||
pct exec "$CT_ID" -- bash -c "cd ${INVIDIOUS_PATH} && docker compose up -d --force-recreate"
|
||||
log "Invidious started"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "deploy-invidious-credentials start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push .env and run compose"
|
||||
log " POSTGRES_USER=$POSTGRES_USER"
|
||||
log " POSTGRES_PASSWORD=***"
|
||||
log " INVIDIOUS_COMPANION_KEY=***"
|
||||
log " HMAC_KEY=***"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=$(gen_env)
|
||||
trap "rm -f $tmp" EXIT
|
||||
push_env_atomic "$tmp"
|
||||
run_compose
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
125
scripts/deploy-nextcloud-credentials.sh
Normal file
125
scripts/deploy-nextcloud-credentials.sh
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
# deploy-nextcloud-credentials.sh — деплой кредов Nextcloud в CT 101
|
||||
# Секреты из Vaultwarden (объект NEXTCLOUD). Атомарная запись .env, обновление config.php через occ.
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-nextcloud-credentials.sh
|
||||
# /root/scripts/deploy-nextcloud-credentials.sh --dry-run
|
||||
#
|
||||
# Ротация: сменил пароль/ключи в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master (chmod 600)
|
||||
|
||||
set -e
|
||||
|
||||
CT_ID=101
|
||||
NEXTCLOUD_PATH="/opt/nextcloud"
|
||||
CONFIG_PATH="/mnt/nextcloud-data/html/config/config.php"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
get_secrets() {
|
||||
local item
|
||||
item=$(bw get item "NEXTCLOUD" 2>/dev/null)
|
||||
POSTGRES_PASSWORD=$(bw get password "NEXTCLOUD" 2>/dev/null)
|
||||
NEXTCLOUD_TRUSTED_DOMAINS=$(echo "$item" | jq -r '.fields[] | select(.name=="NEXTCLOUD_TRUSTED_DOMAINS") | .value // empty')
|
||||
DBPASSWORD=$(echo "$item" | jq -r '.fields[] | select(.name=="dbpassword") | .value // empty')
|
||||
SECRET=$(echo "$item" | jq -r '.fields[] | select(.name=="secret") | .value // empty')
|
||||
PASSWORDSALT=$(echo "$item" | jq -r '.fields[] | select(.name=="passwordsalt") | .value // empty')
|
||||
INSTANCEID=$(echo "$item" | jq -r '.fields[] | select(.name=="instanceid") | .value // empty')
|
||||
|
||||
if [ -z "$POSTGRES_PASSWORD" ]; then
|
||||
err "NEXTCLOUD: missing password (POSTGRES_PASSWORD)"
|
||||
exit 1
|
||||
fi
|
||||
NEXTCLOUD_TRUSTED_DOMAINS="${NEXTCLOUD_TRUSTED_DOMAINS:-cloud.katykhin.ru 192.168.1.101}"
|
||||
}
|
||||
|
||||
gen_env() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp" << EOF
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS}
|
||||
EOF
|
||||
echo "$tmp"
|
||||
}
|
||||
|
||||
push_env_atomic() {
|
||||
local tmp="$1"
|
||||
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${NEXTCLOUD_PATH}/.env.tmp && chmod 600 ${NEXTCLOUD_PATH}/.env.tmp && mv ${NEXTCLOUD_PATH}/.env.tmp ${NEXTCLOUD_PATH}/.env"
|
||||
log ".env written (atomic), chmod 600"
|
||||
}
|
||||
|
||||
push_compose() {
|
||||
local compose_src="${SCRIPT_DIR}/nextcloud/docker-compose.yml"
|
||||
if [ -f "$compose_src" ]; then
|
||||
pct push "$CT_ID" "$compose_src" "${NEXTCLOUD_PATH}/docker-compose.yml"
|
||||
log "docker-compose.yml pushed"
|
||||
fi
|
||||
}
|
||||
|
||||
update_config_occ() {
|
||||
[ -n "$DBPASSWORD" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set dbpassword --value="$DBPASSWORD" 2>/dev/null || true
|
||||
[ -n "$SECRET" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set secret --value="$SECRET" 2>/dev/null || true
|
||||
[ -n "$PASSWORDSALT" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set passwordsalt --value="$PASSWORDSALT" 2>/dev/null || true
|
||||
[ -n "$INSTANCEID" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set instanceid --value="$INSTANCEID" 2>/dev/null || true
|
||||
log "config.php updated via occ"
|
||||
}
|
||||
|
||||
run_compose() {
|
||||
pct exec "$CT_ID" -- bash -c "cd ${NEXTCLOUD_PATH} && docker compose up -d --force-recreate"
|
||||
log "Nextcloud started"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "deploy-nextcloud-credentials start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push .env, compose, update config, run compose"
|
||||
log " POSTGRES_PASSWORD=***"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=$(gen_env)
|
||||
trap "rm -f $tmp" EXIT
|
||||
push_env_atomic "$tmp"
|
||||
push_compose
|
||||
run_compose
|
||||
update_config_occ
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
129
scripts/deploy-paperless-credentials.sh
Normal file
129
scripts/deploy-paperless-credentials.sh
Normal file
@@ -0,0 +1,129 @@
|
||||
#!/bin/bash
|
||||
# deploy-paperless-credentials.sh — деплой кредов Paperless в CT 104
|
||||
# Секреты из Vaultwarden (объект PAPERLESS). Атомарная запись docker-compose.env.
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-paperless-credentials.sh
|
||||
# /root/scripts/deploy-paperless-credentials.sh --dry-run
|
||||
#
|
||||
# Ротация: сменил пароль/ключи в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master (chmod 600)
|
||||
|
||||
set -e
|
||||
|
||||
CT_ID=104
|
||||
PAPERLESS_PATH="/opt/paperless"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
get_secrets() {
|
||||
local item
|
||||
item=$(bw get item "PAPERLESS" 2>/dev/null)
|
||||
POSTGRES_PASSWORD=$(bw get password "PAPERLESS" 2>/dev/null)
|
||||
PAPERLESS_URL=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_URL") | .value // empty')
|
||||
PAPERLESS_SECRET_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_SECRET_KEY") | .value // empty')
|
||||
PAPERLESS_TIME_ZONE=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_TIME_ZONE") | .value // empty')
|
||||
PAPERLESS_OCR_LANGUAGE=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_OCR_LANGUAGE") | .value // empty')
|
||||
PAPERLESS_OCR_LANGUAGES=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_OCR_LANGUAGES") | .value // empty')
|
||||
|
||||
if [ -z "$POSTGRES_PASSWORD" ]; then
|
||||
err "PAPERLESS: missing password (POSTGRES_PASSWORD)"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$PAPERLESS_SECRET_KEY" ]; then
|
||||
err "PAPERLESS: missing PAPERLESS_SECRET_KEY field"
|
||||
exit 1
|
||||
fi
|
||||
PAPERLESS_URL="${PAPERLESS_URL:-https://docs.katykhin.ru}"
|
||||
PAPERLESS_TIME_ZONE="${PAPERLESS_TIME_ZONE:-Europe/Moscow}"
|
||||
PAPERLESS_OCR_LANGUAGE="${PAPERLESS_OCR_LANGUAGE:-rus+eng}"
|
||||
PAPERLESS_OCR_LANGUAGES="${PAPERLESS_OCR_LANGUAGES:-rus}"
|
||||
}
|
||||
|
||||
gen_env() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp" << EOF
|
||||
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
PAPERLESS_URL=${PAPERLESS_URL}
|
||||
PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY}
|
||||
PAPERLESS_TIME_ZONE=${PAPERLESS_TIME_ZONE}
|
||||
PAPERLESS_OCR_LANGUAGE=${PAPERLESS_OCR_LANGUAGE}
|
||||
PAPERLESS_OCR_LANGUAGES=${PAPERLESS_OCR_LANGUAGES}
|
||||
EOF
|
||||
echo "$tmp"
|
||||
}
|
||||
|
||||
push_env_atomic() {
|
||||
local tmp="$1"
|
||||
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${PAPERLESS_PATH}/docker-compose.env.tmp && chmod 600 ${PAPERLESS_PATH}/docker-compose.env.tmp && mv ${PAPERLESS_PATH}/docker-compose.env.tmp ${PAPERLESS_PATH}/docker-compose.env"
|
||||
log "docker-compose.env written (atomic), chmod 600"
|
||||
}
|
||||
|
||||
push_compose() {
|
||||
local compose_src="${SCRIPT_DIR}/paperless/docker-compose.yml"
|
||||
if [ -f "$compose_src" ]; then
|
||||
pct push "$CT_ID" "$compose_src" "${PAPERLESS_PATH}/docker-compose.yml"
|
||||
log "docker-compose.yml pushed"
|
||||
else
|
||||
log "WARN: ${compose_src} not found, skipping compose push"
|
||||
fi
|
||||
}
|
||||
|
||||
run_compose() {
|
||||
pct exec "$CT_ID" -- bash -c "cd ${PAPERLESS_PATH} && docker compose up -d --force-recreate"
|
||||
log "Paperless started"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "deploy-paperless-credentials start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push docker-compose.env and run compose"
|
||||
log " POSTGRES_PASSWORD=***"
|
||||
log " PAPERLESS_URL=$PAPERLESS_URL"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=$(gen_env)
|
||||
trap "rm -f $tmp" EXIT
|
||||
push_env_atomic "$tmp"
|
||||
push_compose
|
||||
run_compose
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
130
scripts/deploy-rag-credentials.sh
Normal file
130
scripts/deploy-rag-credentials.sh
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
# deploy-rag-credentials.sh — деплой кредов RAG-service в CT 105
|
||||
# Секреты из Vaultwarden (объект RAG_SERVICE). Атомарная запись .env.
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-rag-credentials.sh
|
||||
# /root/scripts/deploy-rag-credentials.sh --dry-run
|
||||
#
|
||||
# Ротация: сменил RAG_API_KEY в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master (chmod 600)
|
||||
# Vaultwarden: создать запись RAG_SERVICE с полем RAG_API_KEY (тип hidden).
|
||||
|
||||
set -e
|
||||
|
||||
CT_ID=105
|
||||
RAG_PATH="/home/rag-service"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
get_secrets() {
|
||||
local item
|
||||
item=$(bw get item "RAG_SERVICE" 2>/dev/null) || {
|
||||
err "RAG_SERVICE not found in Vaultwarden. Create it: type Login, add custom field RAG_API_KEY (hidden)."
|
||||
exit 1
|
||||
}
|
||||
RAG_API_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="RAG_API_KEY") | .value // empty')
|
||||
if [ -z "$RAG_API_KEY" ]; then
|
||||
err "RAG_SERVICE: missing RAG_API_KEY field"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
gen_env() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp" << EOF
|
||||
# RAG Service Configuration (generated from Vaultwarden)
|
||||
|
||||
# Модель
|
||||
RAG_MODEL=sentence-transformers/all-MiniLM-L12-v2
|
||||
RAG_CACHE_DIR=data/models
|
||||
|
||||
# VectorStore
|
||||
RAG_VECTORS_PATH=data/vectors/vectors.npz
|
||||
RAG_MAX_EXAMPLES=10000
|
||||
RAG_SCORE_MULTIPLIER=5.0
|
||||
|
||||
# Батч-обработка
|
||||
RAG_BATCH_SIZE=16
|
||||
|
||||
# Минимальная длина текста
|
||||
RAG_MIN_TEXT_LENGTH=3
|
||||
|
||||
# API настройки
|
||||
RAG_API_HOST=0.0.0.0
|
||||
RAG_API_PORT=8000
|
||||
|
||||
# Безопасность
|
||||
RAG_API_KEY=${RAG_API_KEY}
|
||||
RAG_ALLOW_NO_AUTH=false
|
||||
|
||||
# Автосохранение векторов
|
||||
RAG_AUTOSAVE_INTERVAL=600
|
||||
|
||||
# Логирование
|
||||
LOG_LEVEL=INFO
|
||||
EOF
|
||||
echo "$tmp"
|
||||
}
|
||||
|
||||
push_env_atomic() {
|
||||
local tmp="$1"
|
||||
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${RAG_PATH}/.env.tmp && chmod 600 ${RAG_PATH}/.env.tmp && mv ${RAG_PATH}/.env.tmp ${RAG_PATH}/.env"
|
||||
log ".env written (atomic), chmod 600"
|
||||
}
|
||||
|
||||
run_compose() {
|
||||
pct exec "$CT_ID" -- bash -c "cd ${RAG_PATH} && docker compose up -d --force-recreate"
|
||||
log "RAG-service started"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "deploy-rag-credentials start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push .env and run compose"
|
||||
log " RAG_API_KEY=***"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=$(gen_env)
|
||||
trap "rm -f $tmp" EXIT
|
||||
push_env_atomic "$tmp"
|
||||
run_compose
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
41
scripts/deploy-ssh-keys-homelab.sh
Normal file
41
scripts/deploy-ssh-keys-homelab.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Deploy SSH public key to all LXC containers and VM 200 in homelab.
|
||||
# Run from machine that can reach Proxmox (192.168.1.150).
|
||||
# Usage: ./deploy-ssh-keys-homelab.sh [path-to-public-key]
|
||||
# Default: ~/.ssh/id_rsa.pub or ~/.ssh/id_ed25519.pub
|
||||
|
||||
set -e
|
||||
PROXMOX="${PROXMOX:-root@192.168.1.150}"
|
||||
KEY_FILE="${1:-$HOME/.ssh/id_rsa.pub}"
|
||||
[ -f "$HOME/.ssh/id_ed25519.pub" ] && [ ! -f "$KEY_FILE" ] && KEY_FILE="$HOME/.ssh/id_ed25519.pub"
|
||||
|
||||
if [ ! -f "$KEY_FILE" ]; then
|
||||
echo "Usage: $0 [path-to-public-key]"
|
||||
echo "No key found at $KEY_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CT_IDS="100 101 103 104 105 107 108 109"
|
||||
|
||||
echo "Deploying key from $KEY_FILE to homelab hosts..."
|
||||
|
||||
# Copy key to Proxmox temp, then deploy from there
|
||||
TMP_KEY="/tmp/deploy-ssh-key-$$.pub"
|
||||
scp -q "$KEY_FILE" "$PROXMOX:$TMP_KEY"
|
||||
trap "ssh $PROXMOX 'rm -f $TMP_KEY'" EXIT
|
||||
|
||||
# Proxmox host
|
||||
echo "Proxmox (192.168.1.150)..."
|
||||
ssh "$PROXMOX" "mkdir -p /root/.ssh && chmod 700 /root/.ssh && grep -qF \"\$(cat $TMP_KEY)\" /root/.ssh/authorized_keys 2>/dev/null || cat $TMP_KEY >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys"
|
||||
|
||||
# LXC containers
|
||||
for id in $CT_IDS; do
|
||||
echo "CT $id (192.168.1.$id)..."
|
||||
ssh "$PROXMOX" "pct exec $id -- bash -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' && pct push $id $TMP_KEY /tmp/key.pub && pct exec $id -- bash -c 'grep -qF \"\$(cat /tmp/key.pub)\" /root/.ssh/authorized_keys 2>/dev/null || cat /tmp/key.pub >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && rm /tmp/key.pub'"
|
||||
done
|
||||
|
||||
# VM 200 (admin user; root may be disabled)
|
||||
echo "VM 200 (admin@192.168.1.200)..."
|
||||
ssh "$PROXMOX" "scp -o StrictHostKeyChecking=accept-new $TMP_KEY admin@192.168.1.200:/tmp/key.pub && ssh admin@192.168.1.200 'mkdir -p /home/admin/.ssh /root/.ssh && chmod 700 /home/admin/.ssh /root/.ssh 2>/dev/null; grep -qF \"\$(cat /tmp/key.pub)\" /home/admin/.ssh/authorized_keys 2>/dev/null || cat /tmp/key.pub >> /home/admin/.ssh/authorized_keys; echo \"\$(cat /tmp/key.pub)\" | sudo tee -a /root/.ssh/authorized_keys >/dev/null; chmod 600 /home/admin/.ssh/authorized_keys /root/.ssh/authorized_keys 2>/dev/null; rm /tmp/key.pub'"
|
||||
|
||||
echo "Done. Connect: ssh root@192.168.1.{100,101,103,104,105,107,108,109}, ssh admin@192.168.1.200"
|
||||
111
scripts/deploy-vpn-route-check.sh
Normal file
111
scripts/deploy-vpn-route-check.sh
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# deploy-vpn-route-check.sh — идемпотентный деплой vpn-route-check на CT 100
|
||||
# Секреты берутся из Vaultwarden (объект localhost), .env генерируется на Proxmox и пушится в CT.
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-vpn-route-check.sh # деплой
|
||||
# /root/scripts/deploy-vpn-route-check.sh --dry-run # только проверка, без записи и compose
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master (chmod 600)
|
||||
|
||||
set -e
|
||||
|
||||
CT_ID=100
|
||||
CT_PATH="/opt/docker/vpn-route-check"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
# --- 1. Разблокировка bw (reuse session если возможно)
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
# --- 2. Получить секреты из Vaultwarden (localhost)
|
||||
get_secrets() {
|
||||
local host user pass
|
||||
host=$(bw get item "localhost" 2>/dev/null | jq -r '.fields[] | select(.name=="ROUTER_TELNET_HOST") | .value // empty')
|
||||
user=$(bw get username "localhost" 2>/dev/null)
|
||||
pass=$(bw get password "localhost" 2>/dev/null)
|
||||
|
||||
if [ -z "$user" ] || [ -z "$pass" ]; then
|
||||
err "localhost: missing username or password in Vaultwarden"
|
||||
exit 1
|
||||
fi
|
||||
host="${host:-192.168.1.1}"
|
||||
ROUTER_TELNET_HOST="$host"
|
||||
ROUTER_TELNET_USER="$user"
|
||||
ROUTER_TELNET_PASSWORD="$pass"
|
||||
}
|
||||
|
||||
# --- 3. Сгенерировать .env во временный файл
|
||||
gen_env() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
cat > "$tmp" << EOF
|
||||
ROUTER_TELNET_HOST=${ROUTER_TELNET_HOST}
|
||||
ROUTER_TELNET_USER=${ROUTER_TELNET_USER}
|
||||
ROUTER_TELNET_PASSWORD=${ROUTER_TELNET_PASSWORD}
|
||||
EOF
|
||||
echo "$tmp"
|
||||
}
|
||||
|
||||
# --- 4. Атомарно записать .env в CT 100
|
||||
push_env_to_ct() {
|
||||
local tmp="$1"
|
||||
< "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${CT_PATH}/.env.tmp && chmod 600 ${CT_PATH}/.env.tmp && mv ${CT_PATH}/.env.tmp ${CT_PATH}/.env"
|
||||
log ".env written to CT $CT_ID (atomic)"
|
||||
}
|
||||
|
||||
# --- 5. docker compose up -d
|
||||
run_compose() {
|
||||
pct exec "$CT_ID" -- bash -c "cd ${CT_PATH} && docker compose up -d --force-recreate"
|
||||
log "vpn-route-check started"
|
||||
}
|
||||
|
||||
# --- main
|
||||
main() {
|
||||
log "deploy-vpn-route-check start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push .env and run compose"
|
||||
log " ROUTER_TELNET_HOST=$ROUTER_TELNET_HOST"
|
||||
log " ROUTER_TELNET_USER=$ROUTER_TELNET_USER"
|
||||
log " ROUTER_TELNET_PASSWORD=***"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp=$(gen_env)
|
||||
trap "rm -f $tmp" EXIT
|
||||
push_env_to_ct "$tmp"
|
||||
run_compose
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
95
scripts/deploy-wireguard-credentials.sh
Normal file
95
scripts/deploy-wireguard-credentials.sh
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
# deploy-wireguard-credentials.sh — деплой конфига WireGuard в CT 109
|
||||
# Секреты из Vaultwarden (объект LOCAL_VPN_SERVER_WG, поле wg0_conf — полный конфиг).
|
||||
#
|
||||
# Использование:
|
||||
# /root/scripts/deploy-wireguard-credentials.sh
|
||||
# /root/scripts/deploy-wireguard-credentials.sh --dry-run
|
||||
#
|
||||
# Перед первым запуском: создать в Vaultwarden запись LOCAL_VPN_SERVER_WG,
|
||||
# добавить кастомное поле wg0_conf (hidden) с полным содержимым /etc/wireguard/wg0.conf.
|
||||
#
|
||||
# Ротация: сменил ключи в Vaultwarden → запустил скрипт → systemctl restart wg-quick@wg0
|
||||
#
|
||||
# Требования: bw, jq, /root/.bw-master (chmod 600)
|
||||
|
||||
set -e
|
||||
|
||||
CT_ID=109
|
||||
WG_CONF_PATH="/etc/wireguard/wg0.conf"
|
||||
BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; }
|
||||
|
||||
ensure_bw_unlocked() {
|
||||
local status
|
||||
status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown")
|
||||
if [ "$status" = "unlocked" ]; then
|
||||
log "bw already unlocked, reusing session"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$BW_MASTER_FILE" ]; then
|
||||
err "Missing $BW_MASTER_FILE"
|
||||
exit 1
|
||||
fi
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || {
|
||||
err "bw unlock failed"
|
||||
exit 1
|
||||
}
|
||||
log "bw unlocked"
|
||||
}
|
||||
|
||||
get_secrets() {
|
||||
WG_CONF=$(bw get item "LOCAL_VPN_SERVER_WG" 2>/dev/null | jq -r '.fields[] | select(.name=="wg0_conf") | .value // empty')
|
||||
if [ -z "$WG_CONF" ]; then
|
||||
err "LOCAL_VPN_SERVER_WG not found or missing wg0_conf field. Create it in Vaultwarden, add field wg0_conf with full wg0.conf content."
|
||||
exit 1
|
||||
fi
|
||||
if ! echo "$WG_CONF" | grep -q '\[Interface\]'; then
|
||||
err "wg0_conf: invalid format (expected [Interface] section)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
push_conf() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
echo "$WG_CONF" > "$tmp"
|
||||
pct push "$CT_ID" "$tmp" "${WG_CONF_PATH}.tmp"
|
||||
rm -f "$tmp"
|
||||
pct exec "$CT_ID" -- bash -c "chmod 600 ${WG_CONF_PATH}.tmp && mv ${WG_CONF_PATH}.tmp ${WG_CONF_PATH}"
|
||||
log "wg0.conf written (atomic), chmod 600"
|
||||
}
|
||||
|
||||
restart_wg() {
|
||||
pct exec "$CT_ID" -- systemctl restart wg-quick@wg0
|
||||
log "wg-quick@wg0 restarted"
|
||||
}
|
||||
|
||||
main() {
|
||||
log "deploy-wireguard-credentials start (dry_run=$DRY_RUN)"
|
||||
ensure_bw_unlocked
|
||||
get_secrets
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log "DRY-RUN: would push wg0.conf and restart WireGuard"
|
||||
log " wg0_conf: $(echo "$WG_CONF" | head -3)..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
push_conf
|
||||
restart_wg
|
||||
log "done"
|
||||
}
|
||||
|
||||
main
|
||||
74
scripts/gitea/docker-compose.yml
Normal file
74
scripts/gitea/docker-compose.yml
Normal file
@@ -0,0 +1,74 @@
|
||||
# Шаблон для /opt/gitea/ на CT 103
|
||||
# Секреты в .env (генерируется deploy-gitea-credentials.sh из Vaultwarden).
|
||||
# .env не коммитить.
|
||||
|
||||
services:
|
||||
db:
|
||||
image: docker.io/library/postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
POSTGRES_USER: gitea
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: gitea
|
||||
volumes:
|
||||
- gitea-postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U gitea"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
server:
|
||||
image: docker.gitea.com/gitea:1.25
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
env_file: .env
|
||||
environment:
|
||||
USER_UID: 1000
|
||||
USER_GID: 1000
|
||||
GITEA__database__DB_TYPE: postgres
|
||||
GITEA__database__HOST: db:5432
|
||||
GITEA__database__NAME: gitea
|
||||
GITEA__database__USER: gitea
|
||||
GITEA__database__PASSWD: ${POSTGRES_PASSWORD}
|
||||
GITEA__server__DOMAIN: 192.168.1.103
|
||||
GITEA__server__ROOT_URL: http://192.168.1.103:3000/
|
||||
GITEA__server__SSH_PORT: 2222
|
||||
volumes:
|
||||
- gitea-data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "2222:22"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
runner:
|
||||
image: docker.io/gitea/act_runner:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_healthy
|
||||
env_file: .env
|
||||
environment:
|
||||
GITEA_INSTANCE_URL: http://server:3000
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
|
||||
GITEA_RUNNER_NAME: gitea-103-runner
|
||||
GITEA_RUNNER_LABELS: docker:docker://alpine:latest
|
||||
volumes:
|
||||
- runner-data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
volumes:
|
||||
gitea-data:
|
||||
gitea-postgres:
|
||||
runner-data:
|
||||
20
scripts/healthcheck-ping.sh
Executable file
20
scripts/healthcheck-ping.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Ping Healthchecks после успешного окна бэкапов (Dead man's switch).
|
||||
# Если ping не пришёл — Healthchecks шлёт алерт в Telegram.
|
||||
# Конфиг: /root/.healthchecks.env (HEALTHCHECKS_URL, HEALTHCHECKS_HOMELAB_UUID)
|
||||
|
||||
CONFIG="${HEALTHCHECKS_CONFIG:-/root/.healthchecks.env}"
|
||||
if [ -f "$CONFIG" ]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG"
|
||||
set +a
|
||||
fi
|
||||
|
||||
HC_URL="${HEALTHCHECKS_URL:-https://healthchecks.katykhin.ru}"
|
||||
HC_UUID="${HEALTHCHECKS_HOMELAB_UUID:-}"
|
||||
|
||||
[ -z "$HC_UUID" ] && exit 0
|
||||
|
||||
curl -fsS --retry 3 --max-time 10 "${HC_URL}/ping/${HC_UUID}" >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
23
scripts/healthchecks-docker/.env.example
Normal file
23
scripts/healthchecks-docker/.env.example
Normal file
@@ -0,0 +1,23 @@
|
||||
# Healthchecks на VPS Миран
|
||||
# Копировать: cp .env.example .env
|
||||
|
||||
SITE_ROOT=https://healthchecks.katykhin.ru/healthchecks
|
||||
SECRET_KEY=CHANGE_ME_openssl_rand_hex_32
|
||||
ALLOWED_HOSTS=healthchecks.katykhin.ru,185.147.80.190,localhost
|
||||
|
||||
DB=postgres
|
||||
DB_HOST=db
|
||||
DB_NAME=hc
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=CHANGE_ME_secure_password
|
||||
|
||||
# Свой бот (не @HealthchecksBot!) — создать через @BotFather, username бота
|
||||
TELEGRAM_TOKEN=
|
||||
TELEGRAM_BOT_NAME=YourBotUsername
|
||||
|
||||
REGISTRATION_OPEN=False
|
||||
|
||||
EMAIL_HOST=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=healthchecks@katykhin.ru
|
||||
31
scripts/healthchecks-docker/docker-compose.yml
Normal file
31
scripts/healthchecks-docker/docker-compose.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
# Healthchecks на VPS Миран
|
||||
# Копировать: cp -r scripts/healthchecks-docker /home/prod/healthchecks
|
||||
# cd /home/prod/healthchecks && cp .env.example .env && редактировать .env
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=${DB_NAME:-hc}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
image: healthchecks/healthchecks:latest
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
- DB_NAME=${DB_NAME:-hc}
|
||||
- DB_USER=postgres
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
25
scripts/healthchecks-nginx-server.conf
Normal file
25
scripts/healthchecks-nginx-server.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
# Референс: server block для healthchecks.katykhin.ru (Let's Encrypt, Telegram webhook)
|
||||
# Вставить в nginx.conf после HTTP redirect server block
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name healthchecks.katykhin.ru;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/healthchecks.katykhin.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/healthchecks.katykhin.ru/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
location = / { return 302 /healthchecks/; }
|
||||
location /static/ { proxy_pass http://127.0.0.1:8000/static/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /projects/ { proxy_pass http://127.0.0.1:8000/projects/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /accounts/ { proxy_pass http://127.0.0.1:8000/accounts/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /integrations/ { proxy_pass http://127.0.0.1:8000/integrations/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /ping/ { proxy_pass http://127.0.0.1:8000/ping/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /admin/ { proxy_pass http://127.0.0.1:8000/admin/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /badge/ { proxy_pass http://127.0.0.1:8000/badge/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /checks/ { proxy_pass http://127.0.0.1:8000/checks/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /docs/ { proxy_pass http://127.0.0.1:8000/docs/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /tv/ { proxy_pass http://127.0.0.1:8000/tv/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location = /healthchecks/ { return 302 /healthchecks/accounts/login/; }
|
||||
location = /healthchecks { return 302 /healthchecks/accounts/login/; }
|
||||
location /healthchecks/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
}
|
||||
6
scripts/healthchecks.env.example
Normal file
6
scripts/healthchecks.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# Конфиг для healthcheck-ping.sh (Proxmox)
|
||||
# Копировать: cp healthchecks.env.example /root/.healthchecks.env
|
||||
# UUID — из веб-интерфейса Healthchecks после создания check "homelab-backups"
|
||||
|
||||
HEALTHCHECKS_URL=https://healthchecks.katykhin.ru
|
||||
HEALTHCHECKS_HOMELAB_UUID=
|
||||
84
scripts/invidious/docker-compose.yml
Normal file
84
scripts/invidious/docker-compose.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
# Шаблон для /opt/invidious/docker-compose.yml на CT 107
|
||||
# Секреты в .env (генерируется deploy-invidious-credentials.sh из Vaultwarden).
|
||||
# .env не коммитить.
|
||||
|
||||
services:
|
||||
invidious:
|
||||
image: quay.io/invidious/invidious:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file: .env
|
||||
environment:
|
||||
INVIDIOUS_CONFIG: |
|
||||
db:
|
||||
dbname: invidious
|
||||
user: ${POSTGRES_USER}
|
||||
password: ${POSTGRES_PASSWORD}
|
||||
host: invidious-db
|
||||
port: 5432
|
||||
check_tables: true
|
||||
invidious_companion:
|
||||
- private_url: "http://companion:8282/companion"
|
||||
invidious_companion_key: "${INVIDIOUS_COMPANION_KEY}"
|
||||
external_port: 443
|
||||
domain: "video.katykhin.ru"
|
||||
https_only: true
|
||||
use_pubsub_feeds: true
|
||||
use_innertube_for_captions: true
|
||||
hmac_key: "${HMAC_KEY}"
|
||||
default_user_preferences:
|
||||
default_home: Popular
|
||||
dark_mode: "light"
|
||||
player_style: "youtube"
|
||||
vr_mode: false
|
||||
automatic_instance_redirect: false
|
||||
healthcheck:
|
||||
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/stats || exit 1
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
logging:
|
||||
options:
|
||||
max-size: "1G"
|
||||
max-file: "4"
|
||||
depends_on:
|
||||
invidious-db:
|
||||
condition: service_healthy
|
||||
|
||||
companion:
|
||||
image: quay.io/invidious/invidious-companion:latest
|
||||
env_file: .env
|
||||
environment:
|
||||
SERVER_SECRET_KEY: ${INVIDIOUS_COMPANION_KEY}
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: "1G"
|
||||
max-file: "4"
|
||||
cap_drop:
|
||||
- ALL
|
||||
read_only: true
|
||||
volumes:
|
||||
- companioncache:/var/tmp/youtubei.js:rw
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
invidious-db:
|
||||
image: docker.io/library/postgres:14
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgresdata:/var/lib/postgresql/data
|
||||
- ./config/sql:/config/sql
|
||||
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
|
||||
env_file: .env
|
||||
environment:
|
||||
POSTGRES_DB: invidious
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||
|
||||
volumes:
|
||||
postgresdata:
|
||||
companioncache:
|
||||
52
scripts/nextcloud/docker-compose.yml
Normal file
52
scripts/nextcloud/docker-compose.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
# Шаблон для /opt/nextcloud/ на CT 101
|
||||
# Секреты в .env (генерируется deploy-nextcloud-credentials.sh из Vaultwarden).
|
||||
# .env не коммитить.
|
||||
|
||||
services:
|
||||
db:
|
||||
image: docker.io/library/postgres:16
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /mnt/nextcloud-data/pgdata:/var/lib/postgresql/data
|
||||
env_file: .env
|
||||
environment:
|
||||
POSTGRES_DB: nextcloud
|
||||
POSTGRES_USER: nextcloud
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U nextcloud"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: docker.io/library/redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
nextcloud:
|
||||
image: docker.io/nextcloud:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_started
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- /mnt/nextcloud-data/html:/var/www/html
|
||||
- /mnt/nextcloud-extra:/mnt/nextcloud-extra
|
||||
- /opt/nextcloud/php-uploads.ini:/usr/local/etc/php/conf.d/zz-uploads.ini:ro
|
||||
env_file: .env
|
||||
environment:
|
||||
APACHE_BODY_LIMIT: "0"
|
||||
NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_TRUSTED_DOMAINS}
|
||||
OVERWRITEPROTOCOL: https
|
||||
OVERWRITEHOST: cloud.katykhin.ru
|
||||
OVERWRITECLIURL: https://cloud.katykhin.ru
|
||||
REDIS_HOST: redis
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_DB: nextcloud
|
||||
POSTGRES_USER: nextcloud
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
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
|
||||
@@ -1,15 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Add vault.katykhin.ru → 192.168.1.103:8280 via NPM API + Access List (LAN + VPN only)
|
||||
# Usage: NPM_EMAIL=j3tears100@gmail.com NPM_PASSWORD=xxx ./npm-add-proxy-vault.sh
|
||||
# Usage: NPM_EMAIL=... NPM_PASSWORD=... ./npm-add-proxy-vault.sh
|
||||
# NPM credentials: Vaultwarden, объект NPM_ADMIN (username=email, password)
|
||||
# Run from host that can reach NPM, or: ssh root@192.168.1.150 "pct exec 100 -- bash -s" < scripts/npm-add-proxy-vault.sh
|
||||
# (then set NPM_URL=http://127.0.0.1:81 and NPM_EMAIL/NPM_PASSWORD in env or below)
|
||||
# NPM credentials: see docs/containers/container-100.md
|
||||
# (then set NPM_URL=http://127.0.0.1:81 and NPM_EMAIL/NPM_PASSWORD in env)
|
||||
|
||||
set -e
|
||||
NPM_URL="${NPM_URL:-http://192.168.1.100:81}"
|
||||
API="$NPM_URL/api"
|
||||
NPM_EMAIL="${NPM_EMAIL:-j3tears100@gmail.com}"
|
||||
NPM_PASSWORD="${NPM_PASSWORD:-kqEUubVq02DJTS8}"
|
||||
if [ -z "$NPM_EMAIL" ] || [ -z "$NPM_PASSWORD" ]; then
|
||||
echo "Set NPM_EMAIL and NPM_PASSWORD (from Vaultwarden, объект NPM_ADMIN)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "1. Getting token..."
|
||||
TOKEN=$(curl -s -X POST "$API/tokens" \
|
||||
|
||||
41
scripts/paperless/docker-compose.yml
Normal file
41
scripts/paperless/docker-compose.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
# Шаблон для /opt/paperless/ на CT 104
|
||||
# Секреты в docker-compose.env (генерируется deploy-paperless-credentials.sh из Vaultwarden).
|
||||
# docker-compose.env не коммитить.
|
||||
|
||||
services:
|
||||
broker:
|
||||
image: docker.io/library/redis:8
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:18
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /mnt/paperless-data/pgdata:/var/lib/postgresql
|
||||
env_file: docker-compose.env
|
||||
environment:
|
||||
POSTGRES_DB: paperless
|
||||
POSTGRES_USER: paperless
|
||||
|
||||
webserver:
|
||||
image: ghcr.io/paperless-ngx/paperless-ngx:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- db
|
||||
- broker
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- /mnt/paperless-data/data:/usr/src/paperless/data
|
||||
- /mnt/paperless-data/media:/usr/src/paperless/media
|
||||
- ./export:/usr/src/paperless/export
|
||||
- ./consume:/usr/src/paperless/consume
|
||||
env_file: docker-compose.env
|
||||
environment:
|
||||
PAPERLESS_REDIS: redis://broker:6379
|
||||
PAPERLESS_DBHOST: db
|
||||
|
||||
volumes:
|
||||
redisdata:
|
||||
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/vaultwarden-secrets.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"
|
||||
30
scripts/smartd-notify.sh
Executable file
30
scripts/smartd-notify.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Вызывается smartd при обнаружении проблемы (-M exec).
|
||||
# Аргументы: $1 = device, $2 = type (1=health, 2=usage, 3=fail), $3 = message
|
||||
# См. man smartd.conf
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
DEVICE="${1:-unknown}"
|
||||
TYPE="${2:-}"
|
||||
MSG="${3:-}"
|
||||
# Дополнительный вывод smartd может быть в stdin
|
||||
EXTRA=$(cat 2>/dev/null || true)
|
||||
|
||||
case "$TYPE" in
|
||||
1) SUMMARY="Health check failed" ;;
|
||||
2) SUMMARY="Usage attribute warning" ;;
|
||||
3) SUMMARY="Usage attribute failure" ;;
|
||||
*) SUMMARY="SMART problem" ;;
|
||||
esac
|
||||
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
BODY="Диск $DEVICE: $SUMMARY"
|
||||
[ -n "$MSG" ] && BODY="${BODY}
|
||||
$MSG"
|
||||
[ -n "$EXTRA" ] && BODY="${BODY}
|
||||
|
||||
$EXTRA"
|
||||
"$NOTIFY_SCRIPT" "⚠️ SMART" "$BODY" || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
24
scripts/systemd/README.md
Normal file
24
scripts/systemd/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Systemd unit-файлы для бэкапов и мониторинга
|
||||
|
||||
Копировать на хост Proxmox в `/etc/systemd/system/`:
|
||||
|
||||
```bash
|
||||
cp *.service *.timer /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
```
|
||||
|
||||
Включить все таймеры:
|
||||
|
||||
```bash
|
||||
for t in backup-*.timer notify-vzdump-success.timer verify-*.timer backup-watchdog-timers.timer backup-healthcheck-ping.timer; do
|
||||
systemctl enable --now "$t" 2>/dev/null || true
|
||||
done
|
||||
```
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
systemctl list-timers --all | grep backup
|
||||
```
|
||||
|
||||
Перед миграцией с cron — отключить задания в crontab (`crontab -e`).
|
||||
14
scripts/systemd/backup-ct101-pgdump.service
Normal file
14
scripts/systemd/backup-ct101-pgdump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап БД Nextcloud (CT 101)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Nextcloud PostgreSQL (CT 101)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-ct101-pgdump.sh && echo $(date -Iseconds) > /var/run/backup-ct101-pgdump.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-ct101-pgdump.timer
Normal file
9
scripts/systemd/backup-ct101-pgdump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Nextcloud DB daily at 01:15
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 01:15:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-ct103-gitea-pgdump.service
Normal file
14
scripts/systemd/backup-ct103-gitea-pgdump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап БД Gitea (CT 103)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Gitea PostgreSQL (CT 103)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-ct103-gitea-pgdump.sh && echo $(date -Iseconds) > /var/run/backup-ct103-gitea-pgdump.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-ct103-gitea-pgdump.timer
Normal file
9
scripts/systemd/backup-ct103-gitea-pgdump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Gitea DB daily at 03:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-ct104-pgdump.service
Normal file
14
scripts/systemd/backup-ct104-pgdump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап БД Paperless (CT 104)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Paperless PostgreSQL (CT 104)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-ct104-pgdump.sh && echo $(date -Iseconds) > /var/run/backup-ct104-pgdump.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-ct104-pgdump.timer
Normal file
9
scripts/systemd/backup-ct104-pgdump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Paperless DB daily at 02:30
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 02:30:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-ct105-vectors.service
Normal file
14
scripts/systemd/backup-ct105-vectors.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап векторов RAG (CT 105)
|
||||
|
||||
[Unit]
|
||||
Description=Backup RAG vectors (CT 105)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-ct105-vectors.sh && echo $(date -Iseconds) > /var/run/backup-ct105-vectors.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-ct105-vectors.timer
Normal file
9
scripts/systemd/backup-ct105-vectors.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup RAG vectors daily at 03:30
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:30:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-etc-pve.service
Normal file
14
scripts/systemd/backup-etc-pve.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап /etc/pve и конфигов хоста
|
||||
|
||||
[Unit]
|
||||
Description=Backup Proxmox host config (/etc/pve, interfaces, hosts)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-etc-pve.sh && echo $(date -Iseconds) > /var/run/backup-etc-pve.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-etc-pve.timer
Normal file
9
scripts/systemd/backup-etc-pve.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup etc-pve daily at 02:15
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 02:15:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-healthcheck-ping.service
Normal file
14
scripts/systemd/backup-healthcheck-ping.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Ping Healthchecks после окна бэкапов (Dead man's switch)
|
||||
|
||||
[Unit]
|
||||
Description=Ping Healthchecks (homelab backups)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/healthcheck-ping.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-healthcheck-ping.timer
Normal file
9
scripts/systemd/backup-healthcheck-ping.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Ping Healthchecks daily at 04:35 (after backup window)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:35:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-immich-photos.service
Normal file
14
scripts/systemd/backup-immich-photos.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап библиотеки фото Immich (rsync с VM 200)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Immich photos (rsync from VM 200)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-immich-photos.sh && echo $(date -Iseconds) > /var/run/backup-immich-photos.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-immich-photos.timer
Normal file
9
scripts/systemd/backup-immich-photos.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Immich photos daily at 01:30
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 01:30:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
16
scripts/systemd/backup-restic-yandex-photos.service
Normal file
16
scripts/systemd/backup-restic-yandex-photos.service
Normal file
@@ -0,0 +1,16 @@
|
||||
# Выгрузка /mnt/backup/photos в Yandex S3 через restic
|
||||
|
||||
[Unit]
|
||||
Description=Backup photos to Yandex S3 (restic)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=HOME=/root
|
||||
Environment=PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-restic-yandex-photos.sh && echo $(date -Iseconds) > /var/run/backup-restic-yandex-photos.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-restic-yandex-photos.timer
Normal file
9
scripts/systemd/backup-restic-yandex-photos.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Restic backup photos to Yandex daily at 04:10
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:10:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
16
scripts/systemd/backup-restic-yandex.service
Normal file
16
scripts/systemd/backup-restic-yandex.service
Normal file
@@ -0,0 +1,16 @@
|
||||
# Выгрузка /mnt/backup (без photos) в Yandex S3 через restic
|
||||
|
||||
[Unit]
|
||||
Description=Backup to Yandex S3 (restic, main)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=HOME=/root
|
||||
Environment=PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-restic-yandex.sh && echo $(date -Iseconds) > /var/run/backup-restic-yandex.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-restic-yandex.timer
Normal file
9
scripts/systemd/backup-restic-yandex.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Restic backup to Yandex daily at 04:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-vaultwarden-data.service
Normal file
14
scripts/systemd/backup-vaultwarden-data.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап данных Vaultwarden (CT 103)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Vaultwarden data (CT 103)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-vaultwarden-data.sh && echo $(date -Iseconds) > /var/run/backup-vaultwarden-data.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-vaultwarden-data.timer
Normal file
9
scripts/systemd/backup-vaultwarden-data.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Vaultwarden daily at 02:45
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 02:45:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-vm200-pgdump.service
Normal file
14
scripts/systemd/backup-vm200-pgdump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап БД Immich (VM 200)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Immich PostgreSQL (VM 200)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-vm200-pgdump.sh && echo $(date -Iseconds) > /var/run/backup-vm200-pgdump.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-vm200-pgdump.timer
Normal file
9
scripts/systemd/backup-vm200-pgdump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Immich DB daily at 03:15
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:15:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
17
scripts/systemd/backup-vps-miran.service
Normal file
17
scripts/systemd/backup-vps-miran.service
Normal file
@@ -0,0 +1,17 @@
|
||||
# Копировать на Proxmox: /etc/systemd/system/
|
||||
# systemctl daemon-reload && systemctl enable --now backup-vps-miran.timer
|
||||
# Удалить из cron: 0 1 * * *
|
||||
|
||||
[Unit]
|
||||
Description=Backup VPS Miran (БД бота, voice_users, S3)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Запись .ok только при успехе (для watchdog)
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-vps-miran.sh && echo $(date -Iseconds) > /var/run/backup-vps-miran.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-vps-miran.timer
Normal file
9
scripts/systemd/backup-vps-miran.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup VPS Miran daily at 01:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 01:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-vps-mtproto.service
Normal file
14
scripts/systemd/backup-vps-mtproto.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап конфигов MTProto + сайт (VPS Германия)
|
||||
|
||||
[Unit]
|
||||
Description=Backup VPS MTProto (Germany)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-vps-mtproto.sh && echo $(date -Iseconds) > /var/run/backup-vps-mtproto.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-vps-mtproto.timer
Normal file
9
scripts/systemd/backup-vps-mtproto.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup VPS MTProto daily at 01:45
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 01:45:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-watchdog-timers.service
Normal file
14
scripts/systemd/backup-watchdog-timers.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Watchdog: проверка failed timers и устаревших healthcheck-файлов
|
||||
|
||||
[Unit]
|
||||
Description=Backup watchdog (failed timers, stale .ok files)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/watchdog-timers.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-watchdog-timers.timer
Normal file
9
scripts/systemd/backup-watchdog-timers.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup watchdog daily at 12:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 12:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
19
scripts/systemd/homelab-dashboard.service
Normal file
19
scripts/systemd/homelab-dashboard.service
Normal file
@@ -0,0 +1,19 @@
|
||||
# Дашборд мониторинга homelab (хост, контейнеры, сервисы)
|
||||
# Порт 19998, статика + API + прокси к Netdata
|
||||
|
||||
[Unit]
|
||||
Description=Homelab Dashboard (monitoring)
|
||||
After=network-online.target netdata.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /root/scripts/dashboard/dashboard-server.py
|
||||
WorkingDirectory=/root/scripts/dashboard
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
15
scripts/systemd/notify-vzdump-success.service
Normal file
15
scripts/systemd/notify-vzdump-success.service
Normal file
@@ -0,0 +1,15 @@
|
||||
# Проверка локального vzdump за последние 2 ч, отправка сводки в Telegram
|
||||
# Задание vzdump в Proxmox UI выполняется в 02:00
|
||||
|
||||
[Unit]
|
||||
Description=Notify vzdump success (check dump dir, send Telegram)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/notify-vzdump-success.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/notify-vzdump-success.timer
Normal file
9
scripts/systemd/notify-vzdump-success.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Notify vzdump success daily at 03:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-restore-level1-full-check.service
Normal file
14
scripts/systemd/verify-restore-level1-full-check.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Restic check --read-data (раз в 6 мес: 1 янв и 1 июля)
|
||||
|
||||
[Unit]
|
||||
Description=Verify restic repository (full read-data, semiannual)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-restore-level1.sh full-check
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
10
scripts/systemd/verify-restore-level1-full-check.timer
Normal file
10
scripts/systemd/verify-restore-level1-full-check.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Restic full check semiannual (Jan 1, Jul 1 at 10:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-01-01 10:00:00
|
||||
OnCalendar=*-07-01 10:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-restore-level1-monthly-check.service
Normal file
14
scripts/systemd/verify-restore-level1-monthly-check.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Restic check --read-data-subset=10% (ежемесячно, 1-е число)
|
||||
|
||||
[Unit]
|
||||
Description=Verify restic repository (monthly read-data-subset)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-restore-level1.sh monthly-check
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user