Compare commits
3 Commits
f319133cee
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 604f0c705f | |||
| 53769e6832 | |||
| 16c254510a |
@@ -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/
|
||||
@@ -36,21 +46,21 @@
|
||||
|
||||
| Что | Откуда | Куда (локально) | Когда | Хранение | Уведомление |
|
||||
|-----|--------|------------------|------|----------|--------------|
|
||||
| **VPS Миран (telegram-helper-bot)** | VPS 185.147.80.190: БД `tg-bot-database.db`, каталог `voice_users`, бакет S3 (Miran) | `/mnt/backup/vps/miran/` (db/, voice_users/, s3/) | **01:00** (cron: `backup-vps-miran.sh`) | БД: 14 дней; voice_users и S3 — перезапись | 🖥️ VPS Миран |
|
||||
| **БД Nextcloud (PostgreSQL)** | CT 101, контейнер `nextcloud-db-1` в `/opt/nextcloud` | `/mnt/backup/databases/ct101-nextcloud/` | **01:15** (cron: `backup-ct101-pgdump.sh`) | 14 дней | 🗄️ Nextcloud (БД) |
|
||||
| **Оригиналы фото Immich** | VM 200, `/mnt/data/library/` | `/mnt/backup/photos/library/` | **01:30** (cron: `backup-immich-photos.sh`, rsync) | Все копии (без автоудаления) | 📷 Фото Immich (rsync) |
|
||||
| **Конфиги MTProto (VPS Германия)** | VPS 185.103.253.99: mtg.service, nginx (katykhin.store), Let's Encrypt, `/var/www/katykhin.store` | `/mnt/backup/vps/mtproto-germany/` (архивы mtproto-config-*.tar.gz) | **01:45** (cron: `backup-vps-mtproto.sh`) | 14 дней | 🌐 VPS MTProto (DE) |
|
||||
| **LXC и VM** | Все выбранные контейнеры (100–109) и VM 200 | `/mnt/backup/proxmox/dump/` | **02:00** (задание в Proxmox UI) | По настройкам задания (7 daily, 4 weekly, 6 monthly) | 💾 Backup local (cron 03:00) |
|
||||
| **Конфиги хоста** | `/etc/pve`, `/etc/network/interfaces`, `/etc/hosts`, `/etc/resolv.conf` | `/mnt/backup/proxmox/etc-pve/` | **02:15** (cron: `backup-etc-pve.sh`) | 30 дней | ⚙️ Конфиги хоста |
|
||||
| **БД Paperless (PostgreSQL)** | CT 104, контейнер `paperless-db-1` в `/opt/paperless` | `/mnt/backup/databases/ct104-paperless/` | **02:30** (cron: `backup-ct104-pgdump.sh`) | 14 дней | 🗄️ Paperless (БД) |
|
||||
| **Данные Vaultwarden (пароли)** | CT 103, `/opt/docker/vaultwarden/data` | `/mnt/backup/other/vaultwarden/`; каталог в restic → Yandex | **02:45** (cron: `backup-vaultwarden-data.sh`) | 14 дней | 🔐 Vaultwarden |
|
||||
| **БД Gitea (PostgreSQL)** | CT 103, контейнер `gitea-db-1` в `/opt/gitea` | `/mnt/backup/databases/ct103-gitea/` | **03:00** (cron: `backup-ct103-gitea-pgdump.sh`) | 14 дней | 🗄️ Gitea (БД) |
|
||||
| **БД Immich (PostgreSQL)** | VM 200, контейнер `database` в `/opt/immich` | `/mnt/backup/databases/vm200-immich/` | **03:15** (cron: `backup-vm200-pgdump.sh`) | 14 дней | 🗄️ Immich (БД) |
|
||||
| **Векторы RAG (CT 105)** | CT 105, `/home/rag-service/data/vectors/` (vectors.npz) | `/mnt/backup/other/ct105-vectors/` | **03:30** (cron: `backup-ct105-vectors.sh`) | 14 дней | 📐 Векторы RAG |
|
||||
| **Выгрузка в Yandex (restic)** | `/mnt/backup` без photos | Yandex Object Storage | **04:00** (cron: `backup-restic-yandex.sh`) | 3 daily, 2 weekly, 2 monthly | ☁️ Restic Yandex |
|
||||
| **Выгрузка в Yandex (restic, фото)** | `/mnt/backup/photos` | Yandex Object Storage (тот же репо) | **04:10** (cron: `backup-restic-yandex-photos.sh`) | 3 daily, 2 weekly, 2 monthly | 📷 Restic Yandex (photos) |
|
||||
| **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 фото). **05:00** зарезервировано под плановую перезагрузку сервера. Задание vzdump — из веб-интерфейса Proxmox (Центр обработки данных → Резервная копия).
|
||||
**Окно бэкапов:** внутренние копии — **01:00–03:30**; выгрузка в облако — **04:00** (основной restic), **04:10** (restic фото). **04:35** — ping Healthchecks (Dead man's switch). **05:00** зарезервировано под плановую перезагрузку сервера. Задание vzdump — из веб-интерфейса Proxmox (Центр обработки данных → Резервная копия).
|
||||
|
||||
---
|
||||
|
||||
@@ -406,26 +416,31 @@ restic restore SNAPSHOT_ID --target /mnt/backup/restore-db --path /mnt/backup/da
|
||||
|
||||
---
|
||||
|
||||
## Скрипты на хосте Proxmox
|
||||
## Скрипты и 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>`.
|
||||
|
||||
| Скрипт | Назначение | Cron |
|
||||
|--------|------------|------|
|
||||
| `/root/scripts/backup-vps-miran.sh` | Бэкап VPS Миран: БД бота, voice_users, S3 (Miran) | 0 1 * * * |
|
||||
| `/root/scripts/backup-ct101-pgdump.sh` | Логический дамп БД Nextcloud из CT 101 | 15 1 * * * |
|
||||
| `/root/scripts/backup-immich-photos.sh` | Копирование библиотеки фото Immich (rsync с VM 200) | 30 1 * * * |
|
||||
| `/root/scripts/backup-vps-mtproto.sh` | Копирование конфигов MTProto + сайт с VPS Германия (185.103.253.99) | 45 1 * * * |
|
||||
| `/root/scripts/backup-etc-pve.sh` | Бэкап /etc/pve и конфигов хоста | 15 2 * * * |
|
||||
| `/root/scripts/backup-ct104-pgdump.sh` | Логический дамп БД Paperless из CT 104 | 30 2 * * * |
|
||||
| `/root/scripts/backup-vaultwarden-data.sh` | Копирование данных Vaultwarden (пароли) из CT 103 | 45 2 * * * |
|
||||
| `/root/scripts/backup-ct103-gitea-pgdump.sh` | Логический дамп БД Gitea из CT 103 | 0 3 * * * |
|
||||
| `/root/scripts/notify-vzdump-success.sh` | Проверка локального vzdump за последние 2 ч, отправка сводки в Telegram | 0 3 * * * |
|
||||
| `/root/scripts/backup-vm200-pgdump.sh` | Логический дамп БД Immich с VM 200 | 15 3 * * * |
|
||||
| `/root/scripts/backup-ct105-vectors.sh` | Копирование векторов RAG (vectors.npz) из CT 105 | 30 3 * * * |
|
||||
| `/root/scripts/backup-restic-yandex.sh` | Выгрузка /mnt/backup (без photos) в Yandex S3 (restic), retention 3/2/2 | 0 4 * * * |
|
||||
| `/root/scripts/backup-restic-yandex-photos.sh` | Выгрузка только /mnt/backup/photos в Yandex S3 (тот же репо), retention 3/2/2 | 10 4 * * * |
|
||||
| `/root/scripts/notify-telegram.sh` | Шлюз отправки уведомлений в Telegram (вызывают скрипты бэкапов) | — |
|
||||
| Скрипт | 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** оставлено свободным для плановой перезагрузки сервера.
|
||||
|
||||
@@ -433,7 +448,7 @@ restic restore SNAPSHOT_ID --target /mnt/backup/restore-db --path /mnt/backup/da
|
||||
|
||||
Если дампы БД (Nextcloud, Paperless, Gitea), архив Vaultwarden или векторы RAG (CT 105) получаются по 20 байт — в копию попал только пустой gzip/tar, команда внутри контейнера не отдала данные. Скрипты при размере < 512 байт завершаются с ошибкой и выводят stderr. Для векторов проверьте путь `/home/rag-service/data/vectors` в CT 105: `pct exec 105 -- ls -la /home/rag-service/data/vectors`.
|
||||
|
||||
**Частая причина дампов БД:** PostgreSQL в контейнере требует пароль (md5/scram). Скрипты берут пароль из **Vaultwarden** (Bitwarden CLI `bw`): объекты **NEXTCLOUD** (поле `dbpassword` или пароль), **PAPERLESS**, **GITEA**. На хосте нужны: `bw`, при необходимости `jq`, разблокировка по мастер-паролю из файла `/root/.bw-master` (см. [Переключение скриптов на секреты из Vaultwarden](proxmox-phase1-backup.md#переключение-скриптов-на-секреты-из-vaultwarden) в proxmox-phase1-backup.md).
|
||||
**Частая причина дампов БД:** PostgreSQL в контейнере требует пароль (md5/scram). Скрипты берут пароль из **Vaultwarden** (Bitwarden CLI `bw`): объекты **NEXTCLOUD** (поле `dbpassword` или пароль), **PAPERLESS**, **GITEA**. На хосте нужны: `bw`, при необходимости `jq`, разблокировка по мастер-паролю из файла `/root/.bw-master` (см. [vaultwarden-secrets.md](../vaultwarden-secrets.md)).
|
||||
|
||||
**Проверка вручную (без подавления stderr):** зайти в контейнер и выполнить дамп, чтобы увидеть сообщение об ошибке:
|
||||
|
||||
@@ -452,7 +467,7 @@ pct exec 103 -- ls -la /opt/docker/vaultwarden/data
|
||||
pct exec 103 -- tar cf - -C /opt/docker/vaultwarden data | wc -c
|
||||
```
|
||||
|
||||
**Запуск из cron и доступ к Vaultwarden (bw):** В cron окружение ограничено: часто `PATH` не содержит `/usr/local/bin`, где обычно установлен `bw`. Скрипты дампов БД (ct101, ct104, ct103) в начале задают `export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"`, поэтому при запуске из cron `bw` и `jq` находятся без правки crontab. Нужно: 1) файл с мастер-паролем, например `/root/.bw-master` (chmod 600), и при необходимости переменная `BW_MASTER_PASSWORD_FILE=/root/.bw-master`; 2) один раз с интерактивной сессии: `bw config server https://vault.katykhin.ru`, `bw login` (сохранит сессию в конфиг); 3) при каждом запуске скрипт делает `bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw` и подставляет пароль БД. Если вручную дамп идёт, а из cron — нет, проверьте: наличие `/root/.bw-master`, права доступа, что `bw` доступен по этому PATH (запустите скрипт через `env -i PATH=/usr/local/bin:/usr/bin:/bin /root/scripts/backup-ct103-gitea-pgdump.sh` для имитации cron).
|
||||
**Запуск из 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.
|
||||
|
||||
### Почему размер дампа меньше размера БД на диске
|
||||
|
||||
@@ -477,7 +492,7 @@ pct exec 101 -- docker exec nextcloud-db-1 psql -U nextcloud -d nextcloud -t -c
|
||||
|
||||
## Уведомления в Telegram
|
||||
|
||||
После **успешного** выполнения каждого бэкапа в Telegram отправляется короткое сообщение (заголовок с эмодзи + краткая сводка). Уведомления приходят по завершении соответствующего скрипта; для локального vzdump — по cron в **03:00** (проверка файлов за последние 2 часа).
|
||||
После **успешного** выполнения каждого бэкапа в Telegram отправляется короткое сообщение (заголовок с эмодзи + краткая сводка). Уведомления приходят по завершении соответствующего скрипта; для локального vzdump — таймер `notify-vzdump-success` в **03:00** (проверка файлов за последние 2 часа).
|
||||
|
||||
|
||||
| Заголовок | Когда | Тело сообщения |
|
||||
@@ -516,13 +531,15 @@ chmod 600 /root/.telegram-notify.env
|
||||
|
||||
Если конфига или кредов нет, шлюз тихо выходит с 0 и не ломает вызывающие скрипты.
|
||||
|
||||
**Позже** тот же шлюз можно вызывать с VM 200 или с VPS (например по SSH на хост Proxmox) — отдельно не реализовано, архитектура это допускает.
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [Стратегия бэкапов (фаза 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,377 +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 и обновлять при смене паролей.
|
||||
|
||||
Это не «шаг бэкапа», но обязательная часть восстановления: без паролей восстановленные контейнеры не войдут в сервисы.
|
||||
|
||||
**Инвентаризация секретов для переноса в Vaultwarden** — ниже сводная таблица: где лежат креды сейчас и какой объект в Vaultwarden им соответствует. Команды для получения значений из Vaultwarden и переключение скриптов — в разделе «Получение секретов из Vaultwarden» ниже.
|
||||
|
||||
| Хост / CT / VM | Текущее место | Объект Vaultwarden |
|
||||
|----------------|----------------|---------------------|
|
||||
| Proxmox (хост) | root, пользователи PVE | (в менеджере вручную) |
|
||||
| Proxmox (хост) | `/root/.restic-yandex.env`, `/root/.restic-password` | **RESTIC** (поля: RESTIC_BACKUP_KEY, RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_SECRET_ACCESS_KEY, TELEGRAM_SELF_CHAT_ID) |
|
||||
| Proxmox (хост) | `/root/.telegram-notify.env` | **HOME_BOT_TOKEN** (пароль = токен бота), **RESTIC** (поле TELEGRAM_SELF_CHAT_ID = chat_id) |
|
||||
| CT 100 | `/root/.secrets/certbot/beget.ini` | **beget** (логин, пароль) |
|
||||
| CT 100 | NPM админка | (в менеджере вручную) |
|
||||
| CT 100 | VPN Route Check compose | **localhost** (логин admin, пароль, поле ROUTER_TELNET_HOST) |
|
||||
| CT 100 | custom_ssl / letsencrypt | (восстановление из /etc/letsencrypt; в Vaultwarden не храним) |
|
||||
| CT 101 | Nextcloud compose, config.php | **NEXTCLOUD** (логин nextcloud, пароль; поля: NEXTCLOUD_TRUSTED_DOMAINS, instanceid, passwordsalt, secret, dbpassword) |
|
||||
| CT 103 | Gitea compose, .env, app.ini | **GITEA** (логин gitea, пароль; поля: GITEA__database__DB_TYPE, GITEA__database__HOST, GITEA_RUNNER_REGISTRATION_TOKEN, LFS_JWT_SECRET, INTERNAL_TOKEN) |
|
||||
| CT 103 | CouchDB local.ini | **OBSIDIAN** (логин obsidian, пароль) |
|
||||
| CT 103 | Vaultwarden .env | **VAULTWARDEN** (пароль = ADMIN_TOKEN, поле SIGNUPS_ALLOWED) |
|
||||
| CT 104 | Paperless compose, docker-compose.env | **PAPERLESS** (логин paperless, пароль; поля: PAPERLESS_URL, PAPERLESS_SECRET_KEY, PAPERLESS_TIME_ZONE, PAPERLESS_OCR_LANGUAGE, PAPERLESS_OCR_LANGUAGES) |
|
||||
| CT 107 | Invidious compose | **INVIDIOUS** (логин kemal, пароль; поля: SERVER_SECRET_KEY, test) |
|
||||
| CT 108 | ice-servers.json | **GALENE** (поле config — JSON TURN) |
|
||||
| VM 200 | `/opt/immich/.env` | **IMMICH** (логин/пароль и поля по .env) |
|
||||
| VM 200 | `/opt/immich-deduper/.env` | **IMMICH_DEDUPER** (логин postgres, пароль; поля: DEDUP_PORT, DEDUP_DATA, DEDUP_IMAGE, IMMICH_PATH, PSQL_HOST, PSQL_PORT, PSQL_DB) |
|
||||
| Proxmox (хост) | `/root/.vps-miran-s3.env` | **MIRAN_S3** (поля: S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME) |
|
||||
|
||||
---
|
||||
|
||||
#### Получение секретов из Vaultwarden
|
||||
|
||||
**Требования:** установлены `bw` (Bitwarden CLI) и `jq`; настроен сервер: `bw config server https://vault.katykhin.ru`. Мастер-пароль задаётся через переменную `BW_MASTER_PASSWORD` или через **файл с доступом только для текущего пользователя** (`chmod 600`), например `/root/.bw-master`; файл не хранить в репозитории. Перед запросами: `bw sync` и разблокировка: `export BW_SESSION=$(bw unlock --passwordenv BW_MASTER_PASSWORD --raw)` или `bw unlock --passwordfile /path/to/file --raw`.
|
||||
|
||||
**Команды по объектам** (выполнять после `bw unlock` в той же сессии):
|
||||
|
||||
| Объект | Логин / пароль | Кастомное поле |
|
||||
|--------|----------------|----------------|
|
||||
| **beget** | `bw get username "beget"`, `bw get password "beget"` | — |
|
||||
| **GALENE** | — | `bw get item "GALENE" \| jq -r '.fields[] \| select(.name=="config") \| .value'` |
|
||||
| **GITEA** | `bw get username "GITEA"`, `bw get password "GITEA"` | `bw get item "GITEA" \| jq -r '.fields[] \| select(.name=="ИМЯ_ПОЛЯ") \| .value'` — подставить GITEA_RUNNER_REGISTRATION_TOKEN, LFS_JWT_SECRET, INTERNAL_TOKEN, GITEA__database__DB_TYPE, GITEA__database__HOST |
|
||||
| **HOME_BOT_TOKEN** | — | пароль = токен: `bw get password "HOME_BOT_TOKEN"` |
|
||||
| **IMMICH** | по структуре в Vaultwarden | `bw get item "IMMICH" \| jq '.fields'` |
|
||||
| **IMMICH_DEDUPER** | `bw get username "IMMICH_DEDUPER"`, `bw get password "IMMICH_DEDUPER"` | поля DEDUP_*, IMMICH_PATH, PSQL_* — через `jq -r '.fields[] \| select(.name=="X") \| .value'` |
|
||||
| **INVIDIOUS** | `bw get username "INVIDIOUS"`, `bw get password "INVIDIOUS"` | `bw get item "INVIDIOUS" \| jq -r '.fields[] \| select(.name=="SERVER_SECRET_KEY") \| .value'` |
|
||||
| **localhost** | `bw get username "localhost"`, `bw get password "localhost"` | `bw get item "localhost" \| jq -r '.fields[] \| select(.name=="ROUTER_TELNET_HOST") \| .value'` |
|
||||
| **MIRAN_S3** | — | S3_ACCESS_KEY: `bw get item "MIRAN_S3" \| jq -r '.fields[] \| select(.name=="S3_ACCESS_KEY") \| .value'`; аналогично S3_SECRET_KEY, S3_BUCKET_NAME |
|
||||
| **NEXTCLOUD** | `bw get username "NEXTCLOUD"`, `bw get password "NEXTCLOUD"` | secret: `bw get item "NEXTCLOUD" \| jq -r '.fields[] \| select(.name=="secret") \| .value'`; dbpassword, passwordsalt, instanceid, NEXTCLOUD_TRUSTED_DOMAINS — то же с `.name=="..."` |
|
||||
| **OBSIDIAN** | `bw get username "OBSIDIAN"`, `bw get password "OBSIDIAN"` | — |
|
||||
| **PAPERLESS** | `bw get username "PAPERLESS"`, `bw get password "PAPERLESS"` | PAPERLESS_SECRET_KEY, PAPERLESS_URL и др. — `bw get item "PAPERLESS" \| jq -r '.fields[] \| select(.name=="PAPERLESS_SECRET_KEY") \| .value'` |
|
||||
| **RESTIC** | — | RESTIC_BACKUP_KEY: `bw get item "RESTIC" \| jq -r '.fields[] \| select(.name=="RESTIC_BACKUP_KEY") \| .value'`; RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, TELEGRAM_SELF_CHAT_ID — то же с нужным `.name` |
|
||||
| **VAULTWARDEN** | — | пароль = ADMIN_TOKEN: `bw get password "VAULTWARDEN"`; SIGNUPS_ALLOWED — из полей |
|
||||
|
||||
**Универсальный шаблон для поля по имени:**
|
||||
`bw get item "ИМЯ_ОБЪЕКТА" | jq -r '.fields[] | select(.name=="ИМЯ_ПОЛЯ") | .value'`
|
||||
|
||||
---
|
||||
|
||||
#### Переключение скриптов на секреты из Vaultwarden
|
||||
|
||||
Ниже — как перейти с чтения из файлов на подстановку из `bw` на **хосте Proxmox**. Мастер-пароль хранить **только в файле с доступом для текущего пользователя:** `chmod 600 /root/.bw-master` (владелец root — только root читает/пишет); в репозиторий файл не коммитить. Либо задавать переменную окружения при запуске по крону.
|
||||
|
||||
**1. Restic (backup-restic-yandex.sh, backup-restic-yandex-photos.sh, restore-one-vzdump-from-restic.sh)**
|
||||
|
||||
Сейчас: `source /root/.restic-yandex.env`, пароль из `/root/.restic-password`.
|
||||
|
||||
Переключение: в начале скрипта (после `set -e`) разблокировать BW и выставить переменные из объекта **RESTIC**:
|
||||
|
||||
```bash
|
||||
# Разблокировать Vaultwarden (мастер-пароль из файла с chmod 600 или env)
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw)
|
||||
fi
|
||||
# Подставить секреты из RESTIC
|
||||
ITEM=$(bw get item "RESTIC")
|
||||
export RESTIC_REPOSITORY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value')
|
||||
export AWS_ACCESS_KEY_ID=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value')
|
||||
export AWS_SECRET_ACCESS_KEY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value')
|
||||
export AWS_DEFAULT_REGION=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value')
|
||||
RESTIC_PASSWORD=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value')
|
||||
export RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||||
echo -n "$RESTIC_PASSWORD" > "$RESTIC_PASSWORD_FILE"
|
||||
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||||
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT
|
||||
```
|
||||
|
||||
Убрать из скрипта: проверку/чтение `ENV_FILE` и `RESTIC_PASSWORD_FILE` из файлов; оставить использование переменных `RESTIC_REPOSITORY`, `AWS_*`, `RESTIC_PASSWORD_FILE` как выше. Для restore-one-vzdump-from-restic.sh — тот же блок в начале.
|
||||
|
||||
**2. Telegram (notify-telegram.sh)**
|
||||
|
||||
Сейчас: `source /root/.telegram-notify.env`, переменные `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`.
|
||||
|
||||
Переключение: читать токен из **HOME_BOT_TOKEN** (пароль), chat_id из объекта **RESTIC** (поле TELEGRAM_SELF_CHAT_ID). В начале скрипта (если нет уже разблокировки):
|
||||
|
||||
```bash
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw)
|
||||
fi
|
||||
TELEGRAM_BOT_TOKEN=$(bw get password "HOME_BOT_TOKEN")
|
||||
TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value')
|
||||
```
|
||||
|
||||
Дальше в скрипте использовать `TELEGRAM_BOT_TOKEN` и `TELEGRAM_CHAT_ID` как раньше (проверка на пустоту, вызов curl). Файл `/root/.telegram-notify.env` можно не использовать.
|
||||
|
||||
**3. Дампы БД (backup-ct101-pgdump.sh, backup-ct104-pgdump.sh, backup-ct103-gitea-pgdump.sh)**
|
||||
|
||||
Скрипты уже берут PGPASSWORD из Vaultwarden: в начале разблокировка `bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw`, затем для pg_dump передаётся `-e PGPASSWORD=...` в `docker exec`. Источники паролей:
|
||||
|
||||
- **Nextcloud (CT 101):** объект **NEXTCLOUD** — поле `dbpassword` или пароль записи (`bw get password "NEXTCLOUD"`).
|
||||
- **Paperless (CT 104):** объект **PAPERLESS** — пароль (`bw get password "PAPERLESS"`).
|
||||
- **Gitea (CT 103):** объект **GITEA** — пароль (`bw get password "GITEA"`).
|
||||
|
||||
Требования на хосте: `bw`, для Nextcloud — `jq`; файл `/root/.bw-master` с мастер-паролем (chmod 600). При ошибке (дамп < 512 байт) скрипт завершается с кодом 1 и выводит stderr; уведомление в Telegram при ошибке не отправляется.
|
||||
|
||||
**4. Остальные места**
|
||||
|
||||
Конфиги сервисов (Nextcloud config.php, Gitea compose, Paperless .env, Immich .env и т.д.) подставлять вручную при восстановлении или написать небольшие скрипты-обёртки, которые один раз получают нужные значения через `bw get item` / `bw get password` и пишут в .env или конфиг. Имена объектов и полей — по таблице выше.
|
||||
|
||||
После переключения: обновить чек-лист (отметить «Секреты перенесены в Vaultwarden») и при необходимости добавить в cron установку `BW_MASTER_PASSWORD_FILE` или вызов разблокировки в общем wrapper’е.
|
||||
|
||||
---
|
||||
|
||||
### Шаг 6. Тестовое восстановление одного контейнера
|
||||
|
||||
Без проверки восстановления нельзя считать стратегию рабочей.
|
||||
|
||||
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).*
|
||||
- [x] Проверен ручной запуск Backup now — файлы появляются в storage. *(рекомендуется проверить разово.)*
|
||||
- [x] Настроен бэкап `/etc/pve` (скрипт + cron) → `/mnt/backup/proxmox/etc-pve`. *(backup-etc-pve.sh, 02:15, 30 дней.)*
|
||||
- [x] Restic: cron на хосте, выгрузка каталогов из `/mnt/backup` в Yandex S3. *(backup-restic-yandex.sh 04:00, backup-restic-yandex-photos.sh 04:10, retention 3 daily / 2 weekly / 2 monthly.)*
|
||||
- [x] Yandex: ключи и endpoint зафиксированы в `/root/.restic-yandex.env`, restic пишет в бакет.
|
||||
- [x] Vaultwarden развёрнут (CT 103).
|
||||
- [ ] Секреты перенесены в Vaultwarden. *(на усмотрение: root PVE, пароли БД, API и т.д.)*
|
||||
- [x] Бэкап данных Vaultwarden включён в restic (Yandex S3). *Локально: backup-vaultwarden-data.sh → `/mnt/backup/other/vaultwarden/`; restic выгружает весь `/mnt/backup` (кроме photos), каталог vaultwarden входит в снимок.*
|
||||
- [x] Выполнено тестовое восстановление одного контейнера (другой VMID), проверена работоспособность. *(26.02.2026: восстановлен CT 107 в слот 999 из `/mnt/backup/proxmox/dump/dump/vzdump-lxc-107-*.tar.zst`, проверены консоль, пинг, Docker, Invidious на 3000; тестовый CT удалён.)*
|
||||
- [x] В документации зафиксирована процедура полного восстановления Proxmox «с нуля». *[backup-howto.md](backup-howto.md): восстановление из vzdump, конфигов, БД, VM 200 с нуля, Vaultwarden, VPS и др.*
|
||||
|
||||
---
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [Архитектура и подключение](../architecture/architecture.md) — хосты, IP, домены.
|
||||
- [Схема сети и зависимости](../network/network-topology.md) — SPOF, зависимость от Proxmox и бэкапов.
|
||||
- [Vaultwarden и использование секретов](../vaultwarden-secrets.md) — установка bw, разблокировка, получение секретов в скриптах.
|
||||
- Документация контейнеров (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). |
|
||||
|
||||
---
|
||||
|
||||
## Осталось сделать
|
||||
|
||||
- **Проверка ручного Backup:** один раз запустить «Backup now» в Proxmox UI (Datacenter → Backup) и убедиться, что файлы появляются в `/mnt/backup/proxmox/dump/dump/`.
|
||||
- **Секреты (по желанию):** перенести пароли/ключи (root PVE, БД, API) в Vaultwarden и обновлять при смене.
|
||||
|
||||
*Выполнено ранее: Yandex + Restic (cron, retention 3/2/2), тестовое восстановление CT 107 → 999 (26.02.2026).*
|
||||
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,7 +56,7 @@
|
||||
|
||||
**Certbot на хосте (внутри CT 100):**
|
||||
- Установлен в системе, таймер `certbot.timer` (проверка продления дважды в день).
|
||||
- Учётные данные Beget API: `/root/.secrets/certbot/beget.ini`.
|
||||
- Учётные данные 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/...`.
|
||||
@@ -181,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
|
||||
```
|
||||
|
||||
---
|
||||
@@ -224,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.
|
||||
@@ -234,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).
|
||||
@@ -245,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%).
|
||||
- [ ] **Резервное копирование:** Регулярный бэкап критичных папок (оценка размеров на момент документации):
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -191,6 +191,53 @@ TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELE
|
||||
# Дальше: 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.
|
||||
@@ -208,14 +255,33 @@ TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELE
|
||||
|
||||
## Инвентаризация записей и полей
|
||||
|
||||
В Vaultwarden удобно хранить записи с именами, совпадающими с сервисами: **RESTIC**, **GITEA**, **PAPERLESS**, **NEXTCLOUD**, **HOME_BOT_TOKEN**, **VAULTWARDEN**, **MIRAN_S3** и т.д. У записей типа «логин» — логин/пароль; у записей с множеством значений — кастомные поля (например `RESTIC_REPOSITORY`, `AWS_ACCESS_KEY_ID`).
|
||||
В 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`).
|
||||
|
||||
Полная таблица «где лежат креды сейчас → какой объект в Vaultwarden» и готовые команды `bw get ...` / `jq` по каждому объекту описаны в [Фаза 1: Стратегия бэкапов](backup/proxmox-phase1-backup.md) — разделы «Инвентаризация секретов для переноса в Vaultwarden», «Получение секретов из Vaultwarden» и «Переключение скриптов на секреты из Vaultwarden».
|
||||
**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.
|
||||
- [Фаза 1: Стратегия бэкапов](backup/proxmox-phase1-backup.md) — инвентаризация секретов, команды по объектам, переключение скриптов на Vaultwarden.
|
||||
- [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) — бэкапы, расписание
|
||||
@@ -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).
|
||||
|
||||
@@ -27,7 +27,7 @@ OUTPUT="$BACKUP_DIR/nextcloud-db-$DATE.sql.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
# PGPASSWORD из Vaultwarden (объект NEXTCLOUD: поле dbpassword или пароль). См. docs/backup/proxmox-phase1-backup.md
|
||||
# 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
|
||||
|
||||
@@ -24,7 +24,7 @@ OUTPUT="$BACKUP_DIR/gitea-db-$DATE.sql.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
# PGPASSWORD из Vaultwarden (объект GITEA, пароль). См. docs/backup/proxmox-phase1-backup.md
|
||||
# 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
|
||||
|
||||
@@ -24,7 +24,7 @@ OUTPUT="$BACKUP_DIR/paperless-db-$DATE.sql.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
# PGPASSWORD из Vaultwarden (объект PAPERLESS, пароль). См. docs/backup/proxmox-phase1-backup.md
|
||||
# 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
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# Запускать на хосте 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"
|
||||
# Время запуска (для логов и уведомлений)
|
||||
@@ -18,7 +20,7 @@ fi
|
||||
# Секреты из Vaultwarden (объект RESTIC)
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
|
||||
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
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
# Перед первым запуском: установить 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}"
|
||||
|
||||
BACKUP_PATH="/mnt/backup"
|
||||
# Время запуска (для логов и уведомлений)
|
||||
@@ -22,7 +25,7 @@ fi
|
||||
# Секреты из Vaultwarden (объект RESTIC)
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
|
||||
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
|
||||
|
||||
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}
|
||||
@@ -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:
|
||||
@@ -30,7 +30,7 @@ fi
|
||||
# Секреты из Vaultwarden (объект RESTIC)
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md"
|
||||
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
|
||||
|
||||
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
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Restic check read-data-subset monthly (1st at 10:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-01 10:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-restore-level1-monthly-dump.service
Normal file
14
scripts/systemd/verify-restore-level1-monthly-dump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Тест restore дампа Nextcloud из restic (ежемесячно)
|
||||
|
||||
[Unit]
|
||||
Description=Verify Nextcloud dump restore from restic (monthly)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-restore-level1.sh monthly-dump
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/verify-restore-level1-monthly-dump.timer
Normal file
9
scripts/systemd/verify-restore-level1-monthly-dump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Verify Nextcloud dump restore monthly (1st at 11:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-01 11:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-restore-level1-weekly.service
Normal file
14
scripts/systemd/verify-restore-level1-weekly.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Restic check (еженедельно)
|
||||
|
||||
[Unit]
|
||||
Description=Verify restic repository (weekly check)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-restore-level1.sh weekly
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/verify-restore-level1-weekly.timer
Normal file
9
scripts/systemd/verify-restore-level1-weekly.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Restic check weekly (Sunday 03:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=Sun *-*-* 03:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-vzdump-level2.service
Normal file
14
scripts/systemd/verify-vzdump-level2.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Автотест vzdump CT 107 (ежемесячно)
|
||||
|
||||
[Unit]
|
||||
Description=Verify vzdump restore (CT 107, monthly)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-vzdump-level2.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/verify-vzdump-level2.timer
Normal file
9
scripts/systemd/verify-vzdump-level2.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Verify vzdump restore monthly (1st at 12:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-01 12:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
147
scripts/verify-restore-level1.sh
Executable file
147
scripts/verify-restore-level1.sh
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/bin/bash
|
||||
# Тест восстановления уровня 1: restic check и проверка дампа Nextcloud из restic.
|
||||
# Запускать на хосте Proxmox под root.
|
||||
# Режимы (аргумент): weekly | monthly-check | full-check | monthly-dump
|
||||
# weekly — restic check (еженедельно)
|
||||
# monthly-check — restic check --read-data-subset=10% (ежемесячно, 1-е число)
|
||||
# full-check — restic check --read-data (раз в 6–12 мес, 1 янв и 1 июля)
|
||||
# monthly-dump — restore дампа Nextcloud из restic, проверка целостности (ежемесячно)
|
||||
# Секреты: из Vaultwarden (объект RESTIC), как в backup-restic-yandex.sh.
|
||||
# Cron/Timer: отдельные таймеры для каждого режима.
|
||||
set -e
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
MODE="${1:-weekly}"
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
RESTORE_TARGET="/tmp/restore-test"
|
||||
RESTIC_PATH_NEXTCLOUD="/mnt/backup/databases/ct101-nextcloud"
|
||||
MIN_DUMP_SIZE_MB=1
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Загрузка кредов restic из Vaultwarden (как в backup-restic-yandex.sh)
|
||||
setup_restic_env() {
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE"
|
||||
return 1
|
||||
fi
|
||||
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Установите bw (Bitwarden CLI) и jq."
|
||||
return 1
|
||||
fi
|
||||
export BW_SESSION
|
||||
BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || return 1
|
||||
RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || return 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
|
||||
[ -z "${!var}" ] && return 1
|
||||
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
|
||||
return 0
|
||||
}
|
||||
|
||||
notify_ok() {
|
||||
[ -x "$NOTIFY_SCRIPT" ] && "$NOTIFY_SCRIPT" "$1" "$2" || true
|
||||
}
|
||||
|
||||
notify_err() {
|
||||
[ -x "$NOTIFY_SCRIPT" ] && "$NOTIFY_SCRIPT" "⚠️ $1" "$2" || true
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
weekly)
|
||||
echo "[verify-restore-level1] Режим: weekly (restic check)"
|
||||
setup_restic_env || { notify_err "Restic check" "Не удалось загрузить креды restic."; exit 1; }
|
||||
if restic check 2>&1; then
|
||||
echo "[verify-restore-level1] restic check OK"
|
||||
# При еженедельном успехе — не спамим (только при ошибке)
|
||||
else
|
||||
notify_err "Restic check" "Ошибка проверки репозитория restic."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
monthly-check)
|
||||
echo "[verify-restore-level1] Режим: monthly-check (restic check --read-data-subset=10%)"
|
||||
setup_restic_env || { notify_err "Restic check (read-data-subset)" "Не удалось загрузить креды restic."; exit 1; }
|
||||
if restic check --read-data-subset=10% 2>&1; then
|
||||
echo "[verify-restore-level1] restic check --read-data-subset=10% OK"
|
||||
notify_ok "Тест restic (read-data-subset)" "OK, 10% данных проверено."
|
||||
else
|
||||
notify_err "Restic check (read-data-subset)" "Ошибка проверки 10% данных репозитория."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
full-check)
|
||||
echo "[verify-restore-level1] Режим: full-check (restic check --read-data)"
|
||||
setup_restic_env || { notify_err "Restic check (read-data)" "Не удалось загрузить креды restic."; exit 1; }
|
||||
if restic check --read-data 2>&1; then
|
||||
echo "[verify-restore-level1] restic check --read-data OK"
|
||||
notify_ok "Тест restic (full read-data)" "OK, полная проверка данных завершена."
|
||||
else
|
||||
notify_err "Restic check (read-data)" "Ошибка полной проверки данных репозитория."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
monthly-dump)
|
||||
echo "[verify-restore-level1] Режим: monthly-dump (restore и проверка дампа Nextcloud)"
|
||||
setup_restic_env || { notify_err "Тест дампа Nextcloud" "Не удалось загрузить креды restic."; exit 1; }
|
||||
rm -rf "$RESTORE_TARGET"
|
||||
mkdir -p "$RESTORE_TARGET"
|
||||
trap 'rm -f "${RESTIC_PASSWORD_FILE:-}" 2>/dev/null; rm -rf "$RESTORE_TARGET"' EXIT INT TERM
|
||||
if ! restic restore latest --target "$RESTORE_TARGET" --path "$RESTIC_PATH_NEXTCLOUD" 2>&1; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка restic restore: не удалось восстановить $RESTIC_PATH_NEXTCLOUD"
|
||||
exit 1
|
||||
fi
|
||||
# Путь после restore: RESTORE_TARGET/mnt/backup/databases/ct101-nextcloud/
|
||||
RESTORED_DIR="$RESTORE_TARGET/mnt/backup/databases/ct101-nextcloud"
|
||||
if [ ! -d "$RESTORED_DIR" ]; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: каталог $RESTORED_DIR не найден после restore."
|
||||
exit 1
|
||||
fi
|
||||
LATEST_SQL=$(ls -t "$RESTORED_DIR"/nextcloud-db-*.sql.gz 2>/dev/null | head -1)
|
||||
if [ -z "$LATEST_SQL" ] || [ ! -f "$LATEST_SQL" ]; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: не найден .sql.gz в $RESTORED_DIR"
|
||||
exit 1
|
||||
fi
|
||||
SIZE_BYTES=$(stat -c%s "$LATEST_SQL" 2>/dev/null || echo 0)
|
||||
SIZE_MB=$(( SIZE_BYTES / 1024 / 1024 ))
|
||||
if [ "$SIZE_MB" -lt "$MIN_DUMP_SIZE_MB" ]; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: размер дампа ${SIZE_MB} MB < ${MIN_DUMP_SIZE_MB} MB (файл: $LATEST_SQL)"
|
||||
exit 1
|
||||
fi
|
||||
if ! gunzip -t "$LATEST_SQL" 2>/dev/null; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: gunzip -t не прошёл для $LATEST_SQL"
|
||||
exit 1
|
||||
fi
|
||||
if ! gunzip -c "$LATEST_SQL" 2>/dev/null | grep -q 'CREATE TABLE'; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: в распакованном дампе нет CREATE TABLE (возможно не SQL дамп)"
|
||||
exit 1
|
||||
fi
|
||||
echo "[verify-restore-level1] Дамп Nextcloud OK: $LATEST_SQL, размер ${SIZE_MB} MB"
|
||||
notify_ok "Тест дампа Nextcloud" "OK, размер ${SIZE_MB} MB."
|
||||
;;
|
||||
*)
|
||||
echo "Использование: $0 {weekly|monthly-check|full-check|monthly-dump}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
88
scripts/verify-vzdump-level2.sh
Executable file
88
scripts/verify-vzdump-level2.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# Тест восстановления уровня 2: автотест vzdump CT 107.
|
||||
# Восстанавливает последний vzdump-lxc-107 в временный CT 999, проверяет запуск, удаляет.
|
||||
# Запускать на хосте Proxmox под root. Ежемесячно (systemd timer).
|
||||
# При успехе/ошибке — уведомление в Telegram.
|
||||
set -e
|
||||
|
||||
DUMP_DIR="/mnt/backup/proxmox/dump/dump"
|
||||
TEST_VMID=999
|
||||
TEST_IP="192.168.1.199/24"
|
||||
TEST_GW="192.168.1.1"
|
||||
STORAGE="local-lvm"
|
||||
WAIT_START_SEC=60
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Очистка при выходе (успех или ошибка)
|
||||
cleanup() {
|
||||
if pct status "$TEST_VMID" &>/dev/null; then
|
||||
echo "[verify-vzdump] Останавливаем и удаляем CT $TEST_VMID..."
|
||||
pct stop "$TEST_VMID" --skiplock 2>/dev/null || true
|
||||
sleep 2
|
||||
pct destroy "$TEST_VMID" --purge 1 --force 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
notify_ok() {
|
||||
[ -x "$NOTIFY_SCRIPT" ] && "$NOTIFY_SCRIPT" "✅ Тест vzdump CT 107" "$1" || true
|
||||
}
|
||||
|
||||
notify_err() {
|
||||
[ -x "$NOTIFY_SCRIPT" ] && "$NOTIFY_SCRIPT" "⚠️ Тест vzdump CT 107" "Ошибка: $1" || true
|
||||
}
|
||||
|
||||
if [ ! -d "$DUMP_DIR" ]; then
|
||||
notify_err "Каталог $DUMP_DIR не найден."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Последний vzdump-lxc-107
|
||||
ARCHIVE=$(ls -t "$DUMP_DIR"/vzdump-lxc-107-*.tar.zst 2>/dev/null | head -1)
|
||||
if [ -z "$ARCHIVE" ] || [ ! -f "$ARCHIVE" ]; then
|
||||
notify_err "Не найден vzdump-lxc-107-*.tar.zst в $DUMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[verify-vzdump] Архив: $ARCHIVE"
|
||||
|
||||
# Убедиться, что CT 999 не существует (остаток от прошлого запуска)
|
||||
if pct status "$TEST_VMID" &>/dev/null; then
|
||||
pct destroy "$TEST_VMID" --purge 1 --force 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
echo "[verify-vzdump] Создаём CT $TEST_VMID из архива..."
|
||||
if ! pct create "$TEST_VMID" "$ARCHIVE" --restore 1 --storage "$STORAGE" 2>&1; then
|
||||
notify_err "pct create не удался."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Другой IP, чтобы не конфликтовать с оригиналом 107
|
||||
echo "[verify-vzdump] Настраиваем сеть (IP $TEST_IP)..."
|
||||
pct set "$TEST_VMID" --net0 "name=eth0,bridge=vmbr0,gw=$TEST_GW,ip=$TEST_IP,type=veth"
|
||||
|
||||
echo "[verify-vzdump] Запускаем CT $TEST_VMID..."
|
||||
if ! pct start "$TEST_VMID" 2>&1; then
|
||||
notify_err "pct start не удался."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[verify-vzdump] Ожидание $WAIT_START_SEC сек..."
|
||||
sleep "$WAIT_START_SEC"
|
||||
|
||||
STATUS=$(pct exec "$TEST_VMID" -- systemctl is-system-running 2>/dev/null || echo "unknown")
|
||||
if [ "$STATUS" != "running" ]; then
|
||||
notify_err "systemctl is-system-running вернул: $STATUS (ожидалось running)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[verify-vzdump] CT 999 запущен, system running. Тест пройден."
|
||||
notify_ok "OK"
|
||||
|
||||
exit 0
|
||||
16
scripts/vpn-route-check/docker-compose.yml
Normal file
16
scripts/vpn-route-check/docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Шаблон для /opt/docker/vpn-route-check/docker-compose.yml на CT 100
|
||||
# Секреты в .env (генерируется deploy-vpn-route-check.sh из Vaultwarden).
|
||||
# .env не коммитить.
|
||||
|
||||
services:
|
||||
vpn-route-check:
|
||||
build: .
|
||||
container_name: vpn-route-check
|
||||
network_mode: host
|
||||
env_file: .env
|
||||
volumes:
|
||||
- vpn-route-check-data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
vpn-route-check-data:
|
||||
58
scripts/watchdog-timers.sh
Executable file
58
scripts/watchdog-timers.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# Watchdog: проверка провалившихся systemd timers.
|
||||
# Запускать раз в день (например 12:00). При наличии failed → notify в Telegram.
|
||||
# Timer: backup-watchdog-timers.timer
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
MAX_AGE_HOURS=24
|
||||
BACKUP_OK_DIR="/var/run"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Проверка systemctl list-timers --failed
|
||||
FAILED=$(systemctl list-timers --failed --no-legend --no-pager 2>/dev/null | grep -v '^$' || true)
|
||||
if [ -n "$FAILED" ]; then
|
||||
MSG="Провалившиеся таймеры:
|
||||
$FAILED"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
"$NOTIFY_SCRIPT" "⚠️ Systemd timers" "$MSG" || true
|
||||
fi
|
||||
echo "[watchdog] Найдены провалившиеся таймеры"
|
||||
echo "$FAILED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Проверка healthcheck-файлов (если файл старше 24 ч — алерт)
|
||||
BACKUP_NAMES="vps-miran ct101-pgdump immich-photos vps-mtproto etc-pve ct104-pgdump vaultwarden-data ct103-gitea-pgdump vm200-pgdump ct105-vectors restic-yandex restic-yandex-photos"
|
||||
STALE=""
|
||||
for name in $BACKUP_NAMES; do
|
||||
OK_FILE="$BACKUP_OK_DIR/backup-$name.ok"
|
||||
if [ -f "$OK_FILE" ]; then
|
||||
AGE_SEC=$(( $(date +%s) - $(stat -c %Y "$OK_FILE" 2>/dev/null || echo 0) ))
|
||||
AGE_HOURS=$(( AGE_SEC / 3600 ))
|
||||
if [ "$AGE_HOURS" -ge "$MAX_AGE_HOURS" ]; then
|
||||
STALE="${STALE}backup-$name.ok (${AGE_HOURS}h)
|
||||
"
|
||||
fi
|
||||
else
|
||||
STALE="${STALE}backup-$name.ok (отсутствует)
|
||||
"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$STALE" ]; then
|
||||
MSG="Файлы .ok старше ${MAX_AGE_HOURS} ч или отсутствуют (последний успешный бэкап):
|
||||
$STALE"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
"$NOTIFY_SCRIPT" "⚠️ Backup watchdog" "$MSG" || true
|
||||
fi
|
||||
echo "[watchdog] Устаревшие healthcheck-файлы"
|
||||
echo "$STALE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[watchdog] OK: таймеры и healthcheck-файлы в порядке"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user