From 92a637ccf94c2773a4afaa2150444246c00616f4 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sun, 20 Sep 2026 16:54:13 +0600 Subject: [PATCH] [2026-09-20] eagle: family/how-to/kraken-access.md family/plans/kraken-backup-to-truenas.md personal/tech/docker-data-root-mount-race.md personal/tech/jellyfin-db-schema-mismatch.md personal/tech/skill-docker-data-root-mount-race.md --- family/how-to/kraken-access.md | 8 +- family/plans/kraken-backup-to-truenas.md | 111 ++++------------ personal/tech/docker-data-root-mount-race.md | 24 ++++ personal/tech/jellyfin-db-schema-mismatch.md | 118 ++++++++++++++++++ .../tech/skill-docker-data-root-mount-race.md | 103 +++++++++++++++ 5 files changed, 278 insertions(+), 86 deletions(-) create mode 100644 personal/tech/jellyfin-db-schema-mismatch.md create mode 100644 personal/tech/skill-docker-data-root-mount-race.md diff --git a/family/how-to/kraken-access.md b/family/how-to/kraken-access.md index 073e7740..1ec524fb 100644 --- a/family/how-to/kraken-access.md +++ b/family/how-to/kraken-access.md @@ -42,7 +42,13 @@ > 5. Проверка: `docker ps` все Up; reverse-канал `udp:reverse:0`; cloudflared tunnel. > 6. Версии Xray на бридже: оставить `26.4.25` + `network_mode: host` (проверенный фикс 2026-09-02) — открытый вопрос к Alex. > -> **Статус (обновлено 2026-09-20): ✅ ФИКС ПРИМЕНЁН, ПРОВЕРЕН, REBOOT-БЕЗОПАСЕН.** +> **Статус (обновлено 2026-09-20): ✅ ФИКС ПРИМЕНЁН, ПРОВЕРЕН РЕАЛЬНЫМ REBOOT'ом, REBOOT-БЕЗОПАСЕН.** +> +> **Подтверждение живым reboot'ом (2026-09-20):** Alex выключил Kraken (`sudo shutdown -h now`), включил — **все 12 контейнеров поднялись сами** (`docker ps` не пуст). Ручных `docker start` не потребовалось. Гонка закрыта. +> +> **Полный разбор механики и фикса:** [[personal/tech/docker-data-root-mount-race]]. +> +> ⚠️ **После reboot `jellyfin` поднялся, но отдавал 503** — это **отдельный** дефект (образ 10.11.8 старше схемы БД 12.1.0.0, `no such column: p.Permission_Permissions_Guid`), НЕ связанный с гонкой. См. [[personal/tech/jellyfin-db-schema-mismatch]]. **Различать при диагностике:** контейнер не поднялся (гонка) vs поднялся, но падает внутри (своя ошибка). > > **Что сделано:** > 1. ✅ Backup: `/home/kraken/backups/docker-recovery-20260920-105228/` (fstab, daemon.json, xray-reverse). diff --git a/family/plans/kraken-backup-to-truenas.md b/family/plans/kraken-backup-to-truenas.md index 2d14f1dc..1f196358 100644 --- a/family/plans/kraken-backup-to-truenas.md +++ b/family/plans/kraken-backup-to-truenas.md @@ -123,119 +123,60 @@ Mail.ru Cloud (зашифровано) **Проверено:** - `docker exec rclone crontab -l` → строка с `#` -- `crontab -l | grep -v '^#' | grep -v '^ - -**Почему обязателен:** контейнер `rclone` на Kraken сейчас льёт `docker` напрямую в `mailru-crypt:kraken/docker` (вс 03:00). После перевода на TrueNAS это надо выключить — иначе двойная нагрузка и тот же 507 (место на Mail.ru переполнено). - -**Что уже сделано:** -- Бэкап текущего состояния: `/home/kraken/backups/rclone-disable-20260920-120606/` — `crontab.bak` (69 б), `backup.sh.bak` (293 б). -- Готовый новый crontab: `/Users/admin/kraken-recovery/rclone-crontab.new` — строка `0 3 * * 0 ...` закомментирована с пояснением и командой отката. - -**Текущий crontab контейнера `rclone`:** -``` -0 3 * * 0 /config/rclone/backup.sh >> /config/rclone/backup.log 2>&1 -``` - -**Команды (нужен апрув Alex — трогают боевой контейнер):** -```bash -# вариант 1 (простой): закомментировать + перечитать -ssh kraken 'sudo docker exec rclone sh -c "sed -i \"s|^0 3 \* \* 0|#0 3 * * 0|\" /config/rclone/crontab && crontab /config/rclone/crontab"' -# вариант 2: docker cp готового файла в контейнер + crontab внутри - -# ОТКАТ: раскомментировать строку и перечитать -ssh kraken 'sudo docker exec rclone crontab /config/rclone/crontab' -``` - -## Шаги реализации (все, кроме 7, выполнены) - -1. Ключ `kraken-backup-push` (ed25519) на Kraken → pub на TrueNAS `authorized_keys` с `from=""`, `no-pty`, `no-port-forwarding`. -2. Создать датасет `/mnt/RED_2TB/backup/kraken` (Passthrough, NFSv4, `nas:nas 770`). -3. Скрипт на Kraken: собрать tar (exclude jellyfin, backup.log) → `rsync -e ssh` на TrueNAS `backup/kraken/`. -4. Ротация 14 архивов (на TrueNAS). -5. Крон на Kraken: `0 4 * * *`. -6. Живой прогон + проверка размера архива (не <4096 байт) + проверка, что уезжает в Mail.ru. -7. **Отключить старый бэкап Kraken → Mail.ru напрямую** (crontab в контейнере `rclone`: закомментировать `run_sync /data/docker mailru-crypt:kraken/docker`), иначе дублирование и переполнение Mail.ru. - -> ⚠️ **П.7 обязателен:** сейчас Kraken льёт `docker` прямо в `mailru-crypt:kraken/docker`. После перевода на TrueNAS это надо выключить — иначе двойная нагрузка и тот же 507. - -## Проверка результата - -```bash -# 1) архив появился на TrueNAS -ssh truenas_admin@mallexxx.duckdns.org 'ls -la /mnt/RED_2TB/backup/kraken/' -# 2) лог прогона -ssh truenas_admin@mallexxx.duckdns.org 'tail -20 /mnt/RED_2TB/backup/kraken/backup.log' -# 3) уезжает в облако (после вс 03:00) -ssh truenas_admin@mallexxx.duckdns.org 'docker exec rclone tail -30 /config/rclone/backup.log' -``` - -## Связанные заметки - -- [[family/plans/t610-backup-to-truenas]] — образец схемы (PULL), питфоллы датасетов/SMB/ключей -- [[family/how-to/truenas-rclone-backup]] — rclone-бэкап TrueNAS → Mail.ru (куда попадает `backup/kraken`) -- [[family/how-to/kraken-access]] — доступ к Kraken, контейнеры -- [[personal/tech/docker-data-root-mount-race]] — гонка docker↔монтирование (причина, почему прогон 20.09 не состоялся) -- [[personal/tech/xray-reverse-tunnel-kraken-truenas]] — разбор того же инцидента -- [[family/how-to/openmediavault-rpi5]] — Kraken OMV -` → **пусто** (активных задач нет) +- `crontab -l | grep -v '^#' | grep -v '^$'` → **пусто** (активных задач нет) - Контейнер `rclone` — `Up`, `crond -f -l 8` жив - `backup.sh` **не тронут** (только расписание отключено) — при необходимости легко вернуть **Откат:** снять `#` + `sudo docker exec rclone crontab /config/rclone/crontab`. Бэкап: `/home/kraken/backups/rclone-disable-20260920-120606/`. -**Причина:** устранение дублирования и переполнения Mail.ru (507). Теперь Kraken идёт в облако **только** через TrueNAS. +**Причина отключения:** устранение дублирования и переполнения Mail.ru (507). Теперь Kraken идёт в облако **только** через TrueNAS. -## ⏸ ШАГ 7 — ИСТОРИЯ (выполнен, оставлено для контекста) +## 🔁 Проверка reboot-безопасности (2026-09-20, после фикса гонки) -**Почему обязателен:** контейнер `rclone` на Kraken сейчас льёт `docker` напрямую в `mailru-crypt:kraken/docker` (вс 03:00). После перевода на TrueNAS это надо выключить — иначе двойная нагрузка и тот же 507 (место на Mail.ru переполнено). +Alex выключил Kraken (`sudo shutdown -h now`). **Фикс mount-race подтверждён живым reboot'ом:** после включения все контейнеры поднялись сами — `docker ps` не пуст (12 Up, включая `cloudflared`, `xray-reverse-bridge`, `rclone`, `jellyfin`). Ручных `docker start` не потребовалось. -**Что уже сделано:** -- Бэкап текущего состояния: `/home/kraken/backups/rclone-disable-20260920-120606/` — `crontab.bak` (69 б), `backup.sh.bak` (293 б). -- Готовый новый crontab: `/Users/admin/kraken-recovery/rclone-crontab.new` — строка `0 3 * * 0 ...` закомментирована с пояснением и командой отката. - -**Текущий crontab контейнера `rclone`:** -``` -0 3 * * 0 /config/rclone/backup.sh >> /config/rclone/backup.log 2>&1 -``` - -**Команды (нужен апрув Alex — трогают боевой контейнер):** +**Что проверять после каждого reboot:** ```bash -# вариант 1 (простой): закомментировать + перечитать -ssh kraken 'sudo docker exec rclone sh -c "sed -i \"s|^0 3 \* \* 0|#0 3 * * 0|\" /config/rclone/crontab && crontab /config/rclone/crontab"' -# вариант 2: docker cp готового файла в контейнер + crontab внутри - -# ОТКАТ: раскомментировать строку и перечитать -ssh kraken 'sudo docker exec rclone crontab /config/rclone/crontab' +ssh kraken 'docker ps -q | wc -l' # ожидаем ~12, не 0 ``` +Если 0 — гонка вернулась, смотреть `systemctl show docker -p RequiresMountsFor` (должно быть непусто). -## Шаги реализации (все, кроме 7, выполнены) +> ⚠️ **Крон не догоняет пропущенное окно.** Пока Kraken лежал, прогон 04:00 пропускается без компенсации. Если это критично — добавить `@reboot`-строку с проверкой «есть ли архив за сегодня». -1. Ключ `kraken-backup-push` (ed25519) на Kraken → pub на TrueNAS `authorized_keys` с `from=""`, `no-pty`, `no-port-forwarding`. -2. Создать датасет `/mnt/RED_2TB/backup/kraken` (Passthrough, NFSv4, `nas:nas 770`). -3. Скрипт на Kraken: собрать tar (exclude jellyfin, backup.log) → `rsync -e ssh` на TrueNAS `backup/kraken/`. -4. Ротация 14 архивов (на TrueNAS). -5. Крон на Kraken: `0 4 * * *`. -6. Живой прогон + проверка размера архива (не <4096 байт) + проверка, что уезжает в Mail.ru. -7. **Отключить старый бэкап Kraken → Mail.ru напрямую** (crontab в контейнере `rclone`: закомментировать `run_sync /data/docker mailru-crypt:kraken/docker`), иначе дублирование и переполнение Mail.ru. +## Шаги реализации (все 7 выполнены) -> ⚠️ **П.7 обязателен:** сейчас Kraken льёт `docker` прямо в `mailru-crypt:kraken/docker`. После перевода на TrueNAS это надо выключить — иначе двойная нагрузка и тот же 507. +1. ✅ Ключ `kraken-backup-push` (ed25519) на Kraken → pub на TrueNAS через `midclt call user.update 72`. +2. ✅ Датасет `/mnt/RED_2TB/backup/kraken` (Passthrough, NFSv4, `nas:nas 770`). +3. ✅ Скрипт на Kraken (`/home/kraken/kraken-backup-to-truenas.sh`): tar (exclude jellyfin, *.log) → `rsync` на TrueNAS. +4. ✅ Ротация 14 архивов (на TrueNAS). +5. ✅ Крон на Kraken: `/etc/cron.d/kraken-backup`, `0 4 * * * root`. +6. ✅ Живой прогон (165 МБ, 36985 файлов, размер совпал). +7. ✅ Старый бэкап Kraken → Mail.ru отключён. + +> **Примечание про `from=` в authorized_keys:** изначально планировалось ограничить ключ по IP, но внешний IP Kraken **динамический** (`92.62.70.9`, в доке был `.41`) → `from=` не ставить, иначе бэкап умрёт при смене IP. Ограничения через API `user.update` всё равно не выставляются — ключ пишется «голым». ## Проверка результата ```bash # 1) архив появился на TrueNAS ssh truenas_admin@mallexxx.duckdns.org 'ls -la /mnt/RED_2TB/backup/kraken/' -# 2) лог прогона -ssh truenas_admin@mallexxx.duckdns.org 'tail -20 /mnt/RED_2TB/backup/kraken/backup.log' +# 2) лог прогона (на Kraken) +ssh kraken 'sudo tail -20 /home/kraken/kraken-backup.log' # 3) уезжает в облако (после вс 03:00) ssh truenas_admin@mallexxx.duckdns.org 'docker exec rclone tail -30 /config/rclone/backup.log' ``` +## Открытые вопросы (не блокируют бэкап) + +- **Mail.ru переполнен** (`507 Insufficient Storage` с 13.09). `backup/kraken` доедет до облака только после освобождения места. TrueNAS-часть работает независимо от этого. +- **Лог `backup.log` контейнера `rclone` = 65 МБ** и растёт — исключён из архива, но стоит рассмотреть ротацию самого лога. + ## Связанные заметки - [[family/plans/t610-backup-to-truenas]] — образец схемы (PULL), питфоллы датасетов/SMB/ключей - [[family/how-to/truenas-rclone-backup]] — rclone-бэкап TrueNAS → Mail.ru (куда попадает `backup/kraken`) - [[family/how-to/kraken-access]] — доступ к Kraken, контейнеры - [[personal/tech/docker-data-root-mount-race]] — гонка docker↔монтирование (причина, почему прогон 20.09 не состоялся) +- [[personal/tech/jellyfin-db-schema-mismatch]] — jellyfin после reboot: сервер старше БД - [[personal/tech/xray-reverse-tunnel-kraken-truenas]] — разбор того же инцидента - [[family/how-to/openmediavault-rpi5]] — Kraken OMV diff --git a/personal/tech/docker-data-root-mount-race.md b/personal/tech/docker-data-root-mount-race.md index 24083390..6388a3dc 100644 --- a/personal/tech/docker-data-root-mount-race.md +++ b/personal/tech/docker-data-root-mount-race.md @@ -81,3 +81,27 @@ After=srv-dev\x2ddisk\x2dby\x2duuid\x2d\x2d....mount ## Урок Любой сервис, зависящий от данных на **медленно монтирующемся диске** (USB-HDD), ОБЯЗАН иметь `RequiresMountsFor`. Без него — гонка при каждом reboot, лотерея. + +## ✅ Фикс подтверждён живым reboot'ом (2026-09-20) + +Alex выключил Kraken (`sudo shutdown -h now`), затем включил. **Все контейнеры поднялись сами** — `docker ps` не пуст (12 Up: `cloudflared`, `xray-reverse-bridge`, `rclone`, `jellyfin`, sonarr/radarr/prowlarr, transmission, portainer, hermes-kraken, flaresolverr, watchtower). Ручных `docker start` не потребовалось. + +**Команда проверки после каждого reboot:** +```bash +ssh kraken 'docker ps -q | wc -l' # ожидаем ~12, НЕ 0 +``` + +Если 0 — гонка вернулась: `systemctl show docker -p RequiresMountsFor` должен быть непуст. + +> ⚠️ **Не путать с другим дефектом:** после этого reboot `jellyfin` поднялся, но отдавал **503** — это **отдельная** проблема (сервер старше БД, `no such column: p.Permission_Permissions_Guid`), НЕ связанная с гонкой монтирования. См. [[personal/tech/jellyfin-db-schema-mismatch]]. При диагностике «сервис не работает после reboot» различать: контейнер не поднялся (гонка) vs поднялся, но падает внутри (своя ошибка приложения). + +## Крон не догоняет пропущенное окно + +Общий питфолл для сервисов на этом узле: если Kraken лежал, cron-задачи (бэкап `0 4 * * *`, t610-подобные) **пропускаются без компенсации**. Для критичных — добавлять `@reboot`-строку с проверкой «артефакт за сегодня уже есть?». + +## Связанные заметки + +- [[personal/tech/jellyfin-db-schema-mismatch]] — jellyfin после reboot: сервер старше БД (503) +- [[family/how-to/kraken-access]] — доступ к Kraken, список контейнеров +- [[family/plans/kraken-backup-to-truenas]] — бэкап стека Kraken на TrueNAS +- [[personal/tech/xray-reverse-tunnel-kraken-truenas]] — падение reverse-туннеля от той же гонки diff --git a/personal/tech/jellyfin-db-schema-mismatch.md b/personal/tech/jellyfin-db-schema-mismatch.md new file mode 100644 index 00000000..97ea24da --- /dev/null +++ b/personal/tech/jellyfin-db-schema-mismatch.md @@ -0,0 +1,118 @@ +# Jellyfin: сервер старше БД — `no such column: p.Permission_Permissions_Guid` + +> **Обнаружено:** 2026-09-20 на Kraken (RPi5/OMV, arm64, Docker). +> **Статус:** 🔴 **НЕ ЗАФИКСИРОВАНО** — диагноз подтверждён, фикс согласован, ждёт апрува Alex. +> **Симптом:** `jellyfin` не подключается (`http://kraken:8096` → **503**), контейнер `Up` но `unhealthy`, health-check бесконечно пишет `Server is could not complete startup. Check logs.` +> **Связанные:** [[personal/tech/docker-data-root-mount-race]], [[family/how-to/kraken-access]], [[family/plans/kraken-backup-to-truenas]] + +## Суть (коротко) + +**БД `jellyfin.db` мигрирована под Jellyfin 12.1.0.0, а запущенный контейнер работает на образе 10.11.8.** +Сервер 10.11.8 ожидает старую схему таблицы `Permissions` (с колонкой `Permission_Permissions_Guid`), а в 12.x-схеме её больше нет → падение на конструкторе `UserManager` → сервер не стартует. + +Это **не** «миграция не прошла». Наоборот: миграции применены **вперёд**, а сервер откатился **назад**. + +## Диагностика (воспроизводимая) + +```bash +# 1) внешний симптом +ssh kraken 'docker exec jellyfin sh -c "curl -s -o /dev/null -w \"%{http_code}\n\" http://localhost:8096/"' +# → 503 + +# 2) статус +ssh kraken 'docker ps --format "{{.Names}} {{.Status}}" | grep jelly' +# → jellyfin Up ... (unhealthy) + +# 3) ГДЕ ЛОГ: jellyfin пишет в ФАЙЛ, не в stdout! +# docker logs пуст по свежему запуску — не путаться. +ssh kraken 'sudo docker exec jellyfin sh -c "ls -lat /config/log/"' +ssh kraken 'sudo docker exec jellyfin sh -c "grep -nE \"SqliteException|could not complete startup\" /config/log/log_20260920.log | head"' +# → SQLite Error 1: 'no such column: p.Permission_Permissions_Guid' + +# 4) какая версия сервера РЕАЛЬНО запущена +ssh kraken 'docker exec jellyfin sh -c "/jellyfin/jellyfin --version"' +# → Jellyfin.Server 10.11.8.0 + +# 5) какой образ у контейнера vs локальный latest +ssh kraken 'docker inspect jellyfin --format "{{.Image}}"' +ssh kraken 'docker inspect jellyfin/jellyfin:latest --format "{{.Id}}"' +# → РАЗНЫЕ ID + +# 6) до какой версии мигрирована БД (на Mac, копия БД) +# см. скрипт ниже: миграции по ProductVersion +``` + +### Разбор БД (скопировать на Mac, не трогая оригинал) + +```bash +ssh kraken 'sudo docker cp jellyfin:/config/data/jellyfin.db /tmp/jf.db && sudo chmod 644 /tmp/jf.db' +scp kraken:/tmp/jf.db /Users/admin/kraken-recovery/jellyfin-db/jellyfin.db +# затем python3: SELECT ProductVersion, COUNT(*) FROM __EFMigrationsHistory GROUP BY ProductVersion +# скрипт-разбор: /Users/admin/kraken-recovery/inspect_db.py, inspect_db2.py +``` + +## Факты инцидента (2026-09-20) + +| Что | Значение | +|---|---| +| Образ **контейнера** | `sha256:1694ff06...` = **10.11.8** | +| Образ **локальный `latest`** | `sha256:78d3ea12...` = **12.1.0.0** (обновлён 15.09) | +| БД `jellyfin.db` мигрирована до | **12.1.0.0** (последняя миграция `20260911120000_StripEmbeddedLinkedChildren`) | +| Миграций в `__EFMigrationsHistory` | 105 (наборы: 9.0.11 ×36, 10.0.11 ×19, 10.11.8 ×32, 12.0.0 ×15, 12.1.0 ×3) | +| Схема `Permissions` в БД | **без** `Permission_Permissions_Guid` (12.x-форма) | +| Контейнер создан | 26.05.2026, **не пересоздавался** | +| Первое падение в логе | **09:57 (до shutdown Kraken)** — дефект не от перезагрузки | +| `SQLiteBackups/` (авто-бэкапы) | **пуст** | +| Единственный бэкап БД | `jellyfin.db.backup-20260617_034429` (17.06) | + +## Как это получилось + +`latest` указывал на 10.11.8 в мае → контейнер создан на этом образе. 15.09 `latest` на Hub обновился до **12.1**. Контейнер в какой-то момент запустился на новом образе (или compose пересоздал его), миграции прошли до 12.1.0.0. Затем контейнер вернулся на **старый** образ 10.11.8 (снапшот от 26.05) — и уже не может стартовать с новой БД. + +**Питфолл тега `latest`:** `docker inspect` показывает digest, откуда образ БЫЛ СКАЧАН, а не текущий `latest`. Локальный «latest» может быть сильно старше, чем `latest` в реестре. Проверять версию **внутри образа**: `docker run --rm --entrypoint sh jellyfin/jellyfin:latest -c '/jellyfin/jellyfin --version'`. + +## Фикс (согласован с Alex, ЖДЁТ ВЫПОЛНЕНИЯ) + +**Миграцию вручную запустить НЕЛЬЗЯ и не нужно** — миграции уже применены (до 12.1.0.0), запускать нечего; даунгрейд схемы EF Core не поддерживает. Проблема не в БД, а в **образе контейнера**. + +```bash +# 1) БЭКАП БД (авто-бэкапов нет!) +ssh kraken 'D=/home/kraken/backups/jellyfin-db-$(date +%Y%m%d-%H%M%S); mkdir -p $D; \ + sudo docker cp jellyfin:/config/data/jellyfin.db $D/ && \ + sudo docker cp jellyfin:/config/data/jellyfin.db-wal $D/ 2>/dev/null; \ + sudo chown -R kraken:kraken $D; ls -la $D' + +# 2) пересоздать контейнер на актуальном образе +ssh kraken 'cd /srv/dev-disk-by-uuid-6194539b-03e0-4f33-986b-4da09834e82f/docker/jellyfin && \ + sudo docker compose up -d --force-recreate' + +# 3) проверка +ssh kraken 'docker exec jellyfin sh -c "/jellyfin/jellyfin --version"' # ожидаем 12.1.0.0 +ssh kraken 'docker ps --format "{{.Names}} {{.Status}}" | grep jelly' # health: healthy +ssh kraken 'docker exec jellyfin sh -c "curl -s -o /dev/null -w \"%{http_code}\n\" http://localhost:8096/"' +``` + +**Риск:** низкий. Данные (`/config`) не трогаются, пересоздаётся только контейнер. Бэкап БД обязателен — откатываться больше некуда. + +**Compose jellyfin** (`/srv/.../docker/jellyfin/docker-compose.yml`): +```yaml +image: jellyfin/jellyfin # без тега = latest +restart: always +ports: ["8096:8096"] +volumes: + - /srv/.../docker/jellyfin:/config + - /srv/.../media:/media:ro +environment: [TZ=Asia/Almaty] +``` +> Рекомендация на будущее: **пинить тег** (`jellyfin/jellyfin:12.1`) вместо `latest`, иначе `watchtower` может снова переключить мажорную версию и сломать БД. + +## Питфоллы диагностики + +- **`docker logs jellyfin` не показывает свежий старт** — jellyfin пишет в `/config/log/log_YYYYMMDD.log`, а не в stdout. Читать файл внутри контейнера. +- **В контейнере нет `sqlite3`, `python3`, `strings`** — БД разбирать только копией на Mac. +- **Старые ошибки в логе сбивают**: `System.UriFormatException: Invalid URI` и `WS ... closed` — это НЕ причина падения, а фон. Искать `SqliteException` и `could not complete startup`. +- Проверять **`--since`** фильтр осторожно: если контейнер старше, `--since 15m` даст пусто. + +## Урок + +При `no such column` в jellyfin (или любом EF Core сервисе) **первым делом сравнивать версию бинарника внутри запущенного контейнера с максимальной версией в `__EFMigrationsHistory`**. Версия БД выше версии сервера = откатился образ. Лечится пересозданием контейнера на правильном образе, НЕ правкой БД. diff --git a/personal/tech/skill-docker-data-root-mount-race.md b/personal/tech/skill-docker-data-root-mount-race.md new file mode 100644 index 00000000..11d58325 --- /dev/null +++ b/personal/tech/skill-docker-data-root-mount-race.md @@ -0,0 +1,103 @@ +# Skill mirror: `docker-data-root-mount-race` + +> Зеркало скилла `docker-data-root-mount-race` (категория `docker-service-administration`) для поиска по vault. +> **Создано:** 2026-09-20. **Источник:** Kraken mount-race инцидент. +> **Связанные:** [[personal/tech/docker-data-root-mount-race]] (полный разбор), [[personal/tech/jellyfin-db-schema-mismatch]], [[family/plans/kraken-backup-to-truenas]] + +## Когда применять + +Хост, у которого Docker `data-root` лежит на **поздно монтирующемся диске** (USB HDD, OMV `/srv/dev-disk-by-uuid-*`, RPi + внешний диск): + +- `docker ps -a` **пусто**, хотя контейнеры были. +- `docker info` → `Containers=0` при смонтированном диске. +- После reboot сервисы в контейнерах «молча down» (туннели, reverse-proxy, jellyfin) — но не удалены. +- Journal: `Duplicate entry in '/etc/fstab'?` + +## Механизм + +`dockerd` стартует **раньше**, чем монтируется диск с `data-root` → видит пустой каталог → загружает пустое состояние (0 контейнеров). Диск монтируется позже, но daemon **не перечитывает**. Данные целы. + +Причина: `docker.service` без `RequiresMountsFor=`. + +## Диагностика + +```bash +docker ps -a --format "{{.Names}}\t{{.Status}}" # пусто +docker info --format "{{.DockerRootDir}} | containers={{.Containers}}" +sudo ls /containers/ | wc -l # >0 = целы +ps -o pid,lstart,cmd -p "$(pgrep -x dockerd)" # РЕАЛЬНОЕ время старта +uptime -s; sudo dmesg | grep -iE "EXT4-fs \(sd[a-z]" | tail -5 +systemctl show docker -p RequiresMountsFor # пусто = гонка возможна +grep -c "" /etc/fstab # 2 = дубль +``` + +**Питфоллы диагностики:** +- `systemctl show docker -p ExecMainStartTimestamp` даёт **stale** время → только `ps -o lstart`. +- `RestartPolicy.Name` в `hostconfig.json` = `null`, но в live-состоянии политика есть. Проверять `docker inspect --format '{{.HostConfig.RestartPolicy.Name}}'`. При `unless-stopped` контейнеры вернутся сами, ручной `docker start` не нужен. + +## Фикс + +**Немедленно:** `sudo systemctl restart docker` (диск уже смонтирован). + +**Постоянно:** drop-in `/etc/systemd/system/docker.service.d/mounts.conf`: +```ini +[Unit] +RequiresMountsFor=/srv/dev-disk-by-uuid-/docker-data +After=srv-dev\x2ddisk\x2dby\x2duuid\x2d.mount +``` ++ `systemctl daemon-reload`, + убрать дубль строки из `/etc/fstab`. + +Проверка: `systemctl show docker -p Requires -p After` → маунт-юнит в **обоих**. + +## Подтверждение реальным reboot + +`systemctl restart docker` — **НЕ** тот же тест, что reboot (диск уже смонтирован). Проверка после реального выключения/включения 2026-09-20: все 12 контейнеров поднялись сами. + +```bash +ssh kraken 'docker ps -q | wc -l' # ожидаем ~12, НЕ 0 +``` + +## 🔴 «Сервис down после reboot» — ДВЕ разные причины, не путать + +| Наблюдение | Причина | Где смотреть | +|---|---|---| +| `docker ps` **пусто** (0) | mount race | этот скилл | +| Контейнер **Up**, но 5xx / `unhealthy` | своя ошибка приложения | лог внутри контейнера | + +Реальный пример путаницы: после reboot `jellyfin` был **Up, но 503** — из-за **несовпадения версий** (образ старше схемы БД), НЕ из-за монтирования. См. [[personal/tech/jellyfin-db-schema-mismatch]]. + +**Правило:** контейнер Up → mount race ни при чём, читать свой лог приложения. + +**Питфолл — логи не в stdout.** `docker logs` может быть пуст, если приложение пишет в файл внутри volume (jellyfin: `/config/log/log_YYYYMMDD.log`). Читать файл; фильтровать старые ошибки по `StartedAt`. + +**Питфолл — дрейф версий у `latest`-тегов.** `docker inspect image:latest` показывает digest, **откуда скачали**, а не текущий `latest`. Локальный `latest` может быть на месяцы старше. Проверять версию внутри образа: `docker run --rm --entrypoint sh :latest -c ' --version'`. Если версия БД выше версии сервера → **пересоздать контейнер на верном образе**, НЕ даунгрейдить данные. + +## Смежная ловушка: смешанные владельцы под data-root + +Бэкап/tar дерева сервисов на том же диске: часть каталогов `root:root`, часть `1000:1000` → обычный юзер получает `Permission denied` и **молча частичный архив**. + +```bash +sudo ls -ldn /../docker/*/ | awk '{print $3, $4, $NF}' # владельцы +``` +Если `sudo -n true` проходит — архивировать под `sudo -n`, а на приёмнике ставить владельца: `rsync --chown=:`. + +**Питфолл — долгий `chown -R`/tar умирает с SSH-сессией.** Запускать отсоединённо: +```bash +sudo systemd-run --unit= --collect --property=Type=oneshot chown -R 1000:1000 +``` + +**Питфолл — `Restarting (1)` + `mkdir: Permission denied`** на data-каталоге: владелец на хосте ≠ UID контейнера. `docker inspect --format '{{.Config.User}}'` → `chown :`. Первые `mkdir`-ошибки могут быть **буферизованным остатком** от прошлого запуска — убедиться, что контейнер реально держится `Up`. + +## Никогда + +- Не считать `docker ps` пустым признаком потери данных — сначала проверить `containers/` count. +- Не `rm -rf` «устаревший» docker-data — это реальные данные. +- Не править `/etc/fstab` через sed — писать файл локально, `findmnt --verify --fstab`, затем копировать. + +## Чеклист после фикса + +- [ ] `docker info` count == число каталогов в `containers/` +- [ ] `RequiresMountsFor` непусто и указывает на `docker-data` +- [ ] `grep -c "" /etc/fstab` == 1 +- [ ] После `systemctl restart docker` критичные контейнеры `Up` +- [ ] После реального reboot: `docker ps -q | wc -l` == ожидаемое, не 0