Files
infra/tasks/grimmory-deployment-plan.md
T
DmitryandClaude Opus 5 c676be81ec Capture current Ansible control plane state
Commit the accumulated infrastructure work that was living only in the
working tree: monitoring stack, emergency access/bot, gyro allocator,
grimmory, adguard, backup audit and the OpenCode agent definitions.

Also ignore Python bytecode, local archives and Nix/direnv artifacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTocXkGUUazHdKKd3r9k71
2026-08-26 21:39:28 +03:00

29 KiB
Raw Blame History

План развертывания Grimmory

Целевая архитектура

  • Отдельный unprivileged LXC grimmory на cloud-pc.
  • Итоговые значения: CTID 149, IP 192.168.1.34. Первоначальный .33 оказался занят после создания LXC.
  • RootFS на storage data, первоначально 64 GB с возможностью расширения.
  • Ресурсы: 2 vCPU, 4 GB RAM, 1 GB swap.
  • Docker с fuse-overlayfs внутри LXC.
  • Docker Compose: Grimmory и MariaDB.
  • Публичный адрес: https://books.ada-dev.ru.
  • Книги и данные находятся внутри диска LXC.
  • Daily PBS snapshot всего CT.
  • Offsite restic: дамп MariaDB, конфигурация и /app/data, но не книги.
  • Образы фиксируются на версиях:
    • grimmory/grimmory:v3.2.4;
    • lscr.io/linuxserver/mariadb:11.4.8.

Размер диска нужно скорректировать до развертывания, если текущая библиотека уже приближается к 40-50 GB.

1. Предварительные проверки

  1. Проверить, что CTID 149 свободен:
    • в Proxmox API;
    • в pct list на обоих узлах;
    • в PBS среди исторических VMID.
  2. Проверить IP 192.168.1.34:
    • отсутствие в inventory;
    • отсутствие DHCP reservation;
    • отсутствие ответа на ARP/ping;
    • отсутствие DNS-записи с другим назначением.
  3. Проверить емкость cloud-pc:data.
  4. Проверить работоспособность PBS datastore и последнего backup job cloud-pc.
  5. Проверить возможность создать DNS-запись books.ada-dev.ru.
  6. Зафиксировать ожидаемый объем существующей коллекции и темп роста.
  7. Получить digest образов после выбора версий. В первой итерации допустим pinned tag; digest предпочтительнее для полностью воспроизводимого состояния.

Проверка должна завершиться без создания LXC. При конфликте CTID/IP выбрать следующие свободные значения и использовать их согласованно во всех файлах.

2. Inventory

Изменить ansible/inventory/hosts.yml:

  • добавить grimmory в lxc_infra;
  • указать ansible_host: 192.168.1.34;
  • указать expected_lan_ip;
  • указать ansible_user: root;
  • использовать существующий ProxyJump через ru-vps;
  • добавить в monitoring_exporters, если Node Exporter разворачивается сразу.

После изменения проверить:

ansible-inventory --graph
ansible-inventory --host grimmory

Edge cases:

  • не использовать IP за пределами маршрутизируемого service range .5-.40;
  • убедиться, что имя grimmory разрешается Ansible именно в новый IP;
  • не добавлять host в inventory до выбора окончательных CTID/IP во всех playbooks.

3. Provisioning LXC

Создать ansible/playbooks/pve-grimmory.yml, используя роль pve_lxc.

Параметры:

pve_lxc_vmid: 149
pve_lxc_node: cloud-pc
pve_lxc_hostname: grimmory
pve_lxc_ip: 192.168.1.34/24
pve_lxc_gateway: 192.168.1.1
pve_lxc_storage: data
pve_lxc_disk: 64
pve_lxc_cores: 2
pve_lxc_memory: 4096
pve_lxc_swap: 1024
pve_lxc_unprivileged: true
pve_lxc_features:
  - nesting=1
  - keyctl=1

Дополнительно настроить Docker-in-LXC по существующему паттерну:

  • разрешить /dev/fuse;
  • добавить bind устройства /dev/fuse;
  • перезапустить LXC только при изменении его PVE-конфигурации;
  • дождаться SSH;
  • проверить наличие пользовательского SSH-ключа.

Текущая роль pve_lxc не управляет /dev/fuse, поэтому эти задачи остаются в playbook, аналогично существующим Docker LXC.

Проверки:

ansible-playbook --syntax-check playbooks/pve-grimmory.yml
ansible-playbook playbooks/pve-grimmory.yml --check

--check для Proxmox API не гарантирует полноценной симуляции, поэтому его результат использовать только как дополнительную проверку.

После создания проверить:

  • pct status 149;
  • unprivileged mode включен;
  • rootfs действительно находится на data;
  • onboot=1;
  • SSH работает через ProxyJump;
  • /dev/fuse существует внутри LXC;
  • повторный запуск playbook не создает изменений.

4. Docker Runtime

Внутри LXC установить:

  • docker.io;
  • Docker Compose v2, если доступен как docker-compose-plugin;
  • fuse-overlayfs;
  • ca-certificates;
  • curl;
  • mariadb-client для backup/restore-проверок;
  • prometheus-node-exporter.

Настроить /etc/docker/daemon.json:

{
  "storage-driver": "fuse-overlayfs"
}

Проверить:

docker info
docker compose version
docker run --rm hello-world

Критерий успеха: Docker сообщает fuse-overlayfs как storage driver и переживает перезапуск LXC.

5. Persistent Data

Создать структуру:

/opt/grimmory/
├── compose.yml
├── .env
├── data/
├── books/
├── bookdrop/
├── mariadb/
└── backup-staging/

Назначение:

  • data -> /app/data;
  • books -> /books;
  • bookdrop -> /bookdrop;
  • mariadb -> /config;
  • backup-staging -> временные согласованные дампы.

Права каталогов должны соответствовать APP_USER_ID, APP_GROUP_ID, MariaDB PUID/PGID.

Edge cases:

  • bookdrop не является библиотекой: после успешного импорта исходники удаляются оттуда;
  • не размещать важные необработанные оригиналы только в bookdrop;
  • не вкладывать bind mounts друг в друга;
  • не использовать DISK_TYPE=NETWORK, поскольку файлы находятся на локальном rootfs;
  • оставлять минимум 15-20% диска свободным: импорт и обработка могут временно удваивать размер книги;
  • учитывать крупные PDF/CBR и возможные heap dump при OOM.

6. Compose

Управлять двумя контейнерами через Compose, поскольку Grimmory зависит от MariaDB. Для этого сервиса Compose логичнее отдельных systemd docker run.

Основные параметры:

  • DISK_TYPE=LOCAL;
  • TZ соответствует HomeLab;
  • ALLOWED_ORIGINS=https://books.ada-dev.ru;
  • MariaDB доступна только во внутренней Docker network;
  • наружу публикуется только Grimmory 6060;
  • порт привязывается к LAN IP LXC, а не 0.0.0.0, если Compose это позволяет;
  • healthcheck использует /api/v1/healthcheck;
  • restart: unless-stopped.

Создать systemd unit-обертку для:

docker compose -f /opt/grimmory/compose.yml up -d --remove-orphans

Не использовать --pull always: обновление должно быть отдельной контролируемой операцией.

Секреты:

  • случайные отдельные пароли DB_PASSWORD и MYSQL_ROOT_PASSWORD;
  • .env с mode 0600;
  • реальные значения не добавляются в Git;
  • .env.example содержит только имена переменных и фиктивные значения;
  • пароль первого администратора создается вручную через UI.

Проверки:

docker compose config
docker compose pull
systemctl start grimmory
docker compose ps
docker compose logs
curl -fsS http://127.0.0.1:6060/api/v1/healthcheck

Критерии:

  • оба контейнера healthy;
  • MariaDB не публикует порт на LAN;
  • после рестарта LXC оба контейнера возвращаются в healthy;
  • логи не содержат Flyway errors, authentication failures и циклических рестартов.

7. Первый запуск

  1. Открыть сервис сначала по LAN.
  2. Создать локального администратора.
  3. Сохранить пароль в password manager.
  4. Отключить неиспользуемую публичную регистрацию.
  5. Создать отдельного обычного пользователя для чтения, если административная учетная запись не должна использоваться на BOOX.
  6. Включить OPDS.
  7. Создать отдельные OPDS credentials.
  8. Выдать пользователю разрешение Access OPDS.
  9. Создать тестовую библиотеку в /books.
  10. Импортировать одну тестовую EPUB через BookDrop.

Важно: OPDS credentials отделены от обычного логина Grimmory. Пароль OPDS после создания нельзя получить повторно.

8. Reverse Proxy

Создать ansible/playbooks/reverse-proxy-grimmory.yml по паттерну Gitea/Vaultwarden.

Caddy site:

books.ada-dev.ru {
    reverse_proxy 192.168.1.34:6060
}

Caddy автоматически поддерживает WebSocket upgrade, но это нужно проверить для /ws.

Не добавлять дополнительный Caddy Basic Auth: он конфликтует с Basic Auth OPDS.

Проверки до переключения DNS:

  • Caddy может достучаться до 192.168.1.34:6060 через OpenVPN;
  • upstream healthcheck возвращает 2xx;
  • Caddyfile проходит caddy validate;
  • конфигурация действительно смонтирована в контейнер Caddy.

После DNS:

curl -fsS https://books.ada-dev.ru/api/v1/healthcheck

Дополнительно проверить:

  • валидную цепочку TLS;
  • WebSocket /ws;
  • правильные внешние Host, scheme и port;
  • отсутствие redirect на внутренний http://192.168.1.34:6060;
  • загрузку файла размером больше стандартного proxy limit;
  • доступ с мобильной сети, а не только из домашнего LAN.

9. OPDS и BOOX Go 7

Каталог:

https://books.ada-dev.ru/api/v1/opds

Настроить в NeoReader:

  • URL корневого каталога;
  • отдельный OPDS username/password;
  • HTTPS;
  • без второго слоя proxy-аутентификации.

Тестовый сценарий:

  1. Каталог открывается.
  2. Отображаются разделы и обложки.
  3. Работает поиск.
  4. EPUB скачивается.
  5. Книга открывается в NeoReader.
  6. Кириллица в названии, авторе и описании отображается корректно.
  7. Повторная загрузка не создает поврежденный файл.
  8. Проверить EPUB, PDF и FB2 отдельно.
  9. Проверить работу из домашнего Wi-Fi и внешней сети.
  10. Проверить поведение после смены OPDS-пароля.

Позиция чтения NeoReader обратно в Grimmory через OPDS не синхронизируется. Это ожидаемое поведение, а не дефект сервера.

10. Firewall

Минимальная политика:

  • SSH 22 только из LAN и OpenVPN;
  • 6060 только из LAN/OpenVPN и от нужного reverse-proxy маршрута;
  • MariaDB не публикуется;
  • остальные входящие соединения запрещены.

Edge case: Docker published ports могут обходить UFW. Поэтому проверить фактические nftables/iptables rules и доступ к 6060 с нежелательных сегментов, а не полагаться только на вывод ufw status.

Публично сервис должен быть доступен только через Caddy на ru-vps, не через router port-forward.

11. Monitoring

Добавить:

  • Node Exporter target 192.168.1.34:9100;
  • LAN blackbox HTTP probe /api/v1/healthcheck;
  • публичный HTTPS probe https://books.ada-dev.ru/api/v1/healthcheck;
  • TLS expiry check;
  • базовые алерты по availability, CPU, RAM, disk и backup freshness.

Не считать один health endpoint достаточным. Он может отвечать при частично неисправной функции OPDS.

Добавить синтетическую OPDS-проверку:

  • Basic Auth тестового monitoring-пользователя;
  • ответ 200;
  • ожидаемый OPDS content type;
  • корректный XML/Atom;
  • секреты только из runtime env, не из шаблона в Git.

Если хранить OPDS credential в monitoring нежелательно, ограничиться публичным healthcheck и ручным OPDS smoke test после обновлений.

12. PBS Backup

Добавить CTID в:

  • ansible/playbooks/pve-backup-jobs.yml, job homelab-pbs-daily-cloud;
  • ansible/roles/backup_audit/defaults/main.yml.

Сохранить исключение Docker merged layers:

/var/lib/docker/fuse-overlayfs/*/merged

Проверки:

  1. Применить backup job.
  2. Запустить первый backup вручную.
  3. Убедиться, что snapshot завершен без warnings.
  4. Проверить наличие VMID 149 в PBS.
  5. Проверить прохождение backup audit.
  6. Проверить размер backup: слишком маленький может означать ошибочное исключение данных.
  7. Выполнить тестовое file-level восстановление нескольких файлов.
  8. Запланировать полный restore drill в отдельный временный CTID.

PBS snapshot MariaDB считается crash-consistent, но не заменяет логический dump.

13. Offsite Restic

Offsite должен содержать:

  • согласованный mariadb-dump;
  • /opt/grimmory/data;
  • compose.yml;
  • зашифрованную или отдельно защищенную конфигурацию;
  • manifest с версиями Grimmory и MariaDB.

Не включать:

  • /opt/grimmory/books;
  • /opt/grimmory/bookdrop;
  • /opt/grimmory/mariadb как raw-copy работающей БД;
  • Docker layers;
  • временные heap dump и cache.

Для дампа:

  • использовать mariadb-dump --single-transaction;
  • сначала писать во временный файл;
  • проверять exit code;
  • атомарно переносить завершенный dump в staging;
  • только после этого запускать restic;
  • удалять старый staging после успешного snapshot.

Расширить существующий restic-паттерн MariaDB-aware логикой, а не использовать SQLite backup helper.

Проверки:

  • mariadb может прочитать dump;
  • restic snapshot содержит dump и /app/data;
  • backup audit отслеживает профиль Grimmory;
  • выполнить пробное восстановление в отдельную временную MariaDB;
  • убедиться, что восстановленная схема проходит Flyway startup на той же версии Grimmory.

14. Обновления

Обновлять только явно, например v3.2.4 -> v3.2.5.

Процесс:

  1. Прочитать release notes и migration warnings.
  2. Проверить свободное место.
  3. Сделать MariaDB dump.
  4. Сделать offsite restic snapshot.
  5. Убедиться в наличии свежего PBS backup.
  6. Изменить pinned image tag/digest.
  7. Pull и restart.
  8. Проверить Flyway logs.
  9. Проверить health, UI, BookDrop и OPDS с BOOX.
  10. Зафиксировать версию в документации.

Не выполнять code-only downgrade после применения Flyway migration. Для отката возвращать одновременно старый image и pre-upgrade backup базы.

15. Edge Cases

Обязательно проверить:

  • заполнение rootfs во время массового импорта;
  • дубликаты и одинаковые ISBN;
  • книги без ISBN;
  • кириллические имена и длинные пути;
  • два файла одной книги в разных форматах;
  • поврежденный EPUB/CBR;
  • файлы больше 1 GB;
  • cross-filesystem BookDrop move;
  • перезапуск во время импорта;
  • падение MariaDB во время обработки;
  • недоступность Google Books/Open Library/Amazon;
  • истечение TLS;
  • смена внешнего домена;
  • потеря OPDS-пароля;
  • потеря admin-пароля;
  • OOM и появление большого heap dump;
  • несовместимая Flyway migration;
  • неожиданное изменение floating Docker tag;
  • частичный backup: БД без /app/data либо наоборот;
  • восстановление БД с файлами библиотеки другого состояния.

16. Финальная приемка

Развертывание считается завершенным, когда:

  • Ansible запускается повторно без изменений;
  • LXC автоматически стартует;
  • контейнеры healthy после reboot;
  • MariaDB не доступна из LAN;
  • HTTPS и WebSocket работают;
  • NeoReader открывает OPDS и скачивает тестовые книги;
  • импорт через BookDrop проходит полностью;
  • Node Exporter и blackbox targets находятся в состоянии UP;
  • создан и проверен PBS backup;
  • создан и тестово восстановлен restic snapshot;
  • секреты отсутствуют в Git diff;
  • Obsidian-документация обновлена.

17. Ожидаемые файлы

Основные изменения:

  • ansible/inventory/hosts.yml;
  • ansible/playbooks/pve-grimmory.yml;
  • ansible/playbooks/reverse-proxy-grimmory.yml;
  • ansible/playbooks/pve-backup-jobs.yml;
  • ansible/roles/backup_audit/defaults/main.yml;
  • ansible/playbooks/offsite-restic-yadisk.yml;
  • ansible/playbooks/backup-audit.yml;
  • ansible/roles/monitoring_server/templates/prometheus.yml.j2;
  • возможно ansible/roles/monitoring_blackbox/templates/push-metrics.sh.j2;
  • ansible/.env.example, только если потребуются новые runtime secrets.

Документация:

  • Notes/Текущее состояние HomeLab после миграции на Proxmox.md;
  • Log.md;
  • Context.md;
  • при необходимости HomeLab.md.

18. Открытый параметр

Перед реализацией подтвердить 64 GB как начальный размер диска или выбрать его по текущему объему коллекции.

Решение: текущая коллекция меньше 20 GB, начальный rootfs оставлен 64 GB.

Журнал выполнения

2026-07-28 — preflight и проектные решения

  • Прочитаны актуальные HomeLab notes и существующие Ansible-паттерны LXC, Caddy, monitoring, PBS и restic.
  • CTID 149 отсутствует в pct list на cloud-pc и mini-pc, а также среди исторических snapshot в PBS.
  • IP 192.168.1.33 отсутствует в inventory и AdGuard config, не отвечает на ping, ARP-состояние с cloud-pcINCOMPLETE; пользователь подтвердил отсутствие DHCP reservation.
  • Storage cloud-pc:data активен: доступно около 836 GB, занято 3.67%.
  • PBS storage и datastore pbs активны; последние snapshots текущих cloud-pc CT созданы 2026-07-27.
  • books.ada-dev.ru уже разрешается в 157.22.231.198 (ru-vps); случайное имя в зоне не разрешается, поэтому это не wildcard. До добавления Caddy site TLS handshake не завершается.
  • Подтверждены grimmory/grimmory:v3.2.4 и lscr.io/linuxserver/mariadb:11.4.8; выбраны OCI index digests sha256:dfa7afdfcf25d649fd664497a62385dd00cd9678c37546e182c172e41c8e80cb и sha256:91de7f701bc7fc3a424b81beafca7a7c6c4c5b7c8be6afd2ae148698695c0b0c.
  • Rootfs подтвержден как data:64, timezone — Europe/Moscow.
  • Пароли MariaDB будут случайно созданы один раз внутри LXC и сохранены только в /opt/grimmory/.env с mode 0600.
  • В первой итерации автоматизируются health/TLS probes; OPDS smoke test остается ручным после создания credentials.
  • Offsite audit первой итерации: атомарный MariaDB dump, проверка наличия dump в snapshot и restic check; автоматический restore в MariaDB отложен.
  • Первый apply остановился до создания из-за quoting условия VMID guard; второй — из-за отсутствия прямого маршрута к Proxmox API. Для API использован временный SSH local-forward через ru-vps.
  • После создания CT выяснился IP conflict: с mini-pc адрес .33 резолвился как YandexStationMini_1DD2.loc и отклонял TCP/22, тогда как локальный bridge cloud-pc видел LXC. Выбран следующий адрес 192.168.1.34; он не отвечает с обоих PVE-узлов, ARP — INCOMPLETE, DNS отсутствует.

2026-07-28 — реализация и автоматическая приёмка

  • Создан unprivileged LXC grimmory CT 149 на cloud-pc: 192.168.1.34/24, data:64, 2 vCPU, 4096 MB RAM, 1024 MB swap, onboot=1, nesting=1, keyctl=1, /dev/fuse.
  • Из-за ограничений API token роль pve_lxc используется в create-only mode для Grimmory; VMID guard запрещает менять чужой CT, а network/keyctl/FUSE идемпотентно управляются через pct на PVE node.
  • Установлены Docker Compose v2 2.26.1, Docker 26.1.5, fuse-overlayfs, MariaDB client, Node Exporter и UFW. Docker подтвердил storage driver fuse-overlayfs.
  • Случайные DB passwords созданы один раз внутри LXC; /opt/grimmory/.env имеет mode 0600, значения не выводились и не добавлялись в Git.
  • Compose запустил pinned Grimmory v3.2.4 и MariaDB 11.4.8; первый startup применил 142 Flyway migrations и занял около 6 минут. Health wait увеличен до 10 минут.
  • После reboot LXC оба контейнера вернулись в healthy; MariaDB не слушает host/LAN port, Grimmory привязан к 192.168.1.34:6060.
  • UFW ограничивает SSH/6060 LAN и OpenVPN, 9100 разрешён только от monitoring LXC. Отдельная GRIMMORY-FILTER chain в DOCKER-USER закрывает обход UFW через Docker published port.
  • Caddy site применён. Проверены public health 200, сертификат Let's Encrypt до 2026-10-26 и WebSocket/STOMP Upgrade 101 на /ws.
  • Prometheus targets: Node Exporter up=1, LAN health probe_success=1, public Grimmory probe probe_success=1; TLS expiry покрыт существующим alert.
  • CT 149 добавлен в homelab-pbs-daily-cloud и PBS audit. Первый PBS backup ct/149/2026-07-28T14:47:32Z успешен, размер 2583749785 bytes; audit success, file-level restore трёх объектов проверен.
  • Ограничение PBS: storage data не поддерживает native LXC snapshots, поэтому job с mode snapshot автоматически перешёл в suspend; фактическая пауза гостя составила около 1 секунды.
  • Создан restic snapshot a3b012ab в Yandex Disk. Включены atomic MariaDB dump, /opt/grimmory/data, Compose, manifest и зашифрованная restore-конфигурация; books/BookDrop/raw MariaDB исключены. restic check и restore непустого grimmory.sql прошли.
  • Повторные runs pve-grimmory.yml, reverse-proxy-grimmory.yml, pve-backup-jobs.yml, Grimmory offsite/audit и limited monitoring завершились с changed=0.
  • Обновлены Obsidian current state, Log.md и Context.md.
  • Reverse DNS для .34 содержит stale имя телефона, но ARP на обоих PVE nodes показывает MAC LXC bc:24:11:43:75:31. Нужно исключить .34 из DHCP pool или закрепить reservation, чтобы предотвратить будущий конфликт.

Требует участия пользователя

  • Создать первого локального администратора и сохранить пароль в password manager.
  • Отключить public registration, создать обычного reader-пользователя.
  • Создать библиотеку /books, выполнить тестовый BookDrop import.
  • Включить OPDS, создать отдельные OPDS credentials и выдать Access OPDS.
  • Проверить EPUB, PDF и FB2 на BOOX Go 7 через Wi-Fi и внешнюю сеть.
  • После создания OPDS пользователя решить, добавлять ли credential в synthetic monitoring; сейчас OPDS smoke test ручной.
  • Создать DHCP reservation/exclusion для 192.168.1.34.
  • Полный restore drill CT и восстановление dump во временную MariaDB остаются отдельной плановой операцией; file-level restore и dump restore-from-restic уже проверены.

2026-07-28 — Caddy KOReader и OPDS compatibility

  • reverse-proxy-grimmory.yml разделяет device endpoints /api/koreader/** и /api/v1/opds/** в отдельный Caddy handler.
  • Для этих endpoints upstream compression отключён и установлен Accept-Encoding: identity; Caddy не добавляет proxy Basic Auth и не изменяет Authorization, X-Auth-*, Range, WWW-Authenticate или download headers.
  • Запросы с Accept-Encoding: gzip, br подтверждённо возвращают 401 без Content-Encoding для KOReader и OPDS. Caddy validate, Grimmory health и idempotent rerun успешны.
  • Дополнительно исправлен Grimmory OPDS 500 из-за No acceptable representation: Caddy нормализует Accept только для Atom/OpenSearch catalog routes, оставляя original Accept для book download/cover routes.