From 4101de5ebcfefcd649e23e4f190be541c429c84c Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sun, 20 Sep 2026 12:25:22 +0600 Subject: [PATCH] [2026-09-20] eagle: family/plans/kraken-backup-to-truenas.md personal/tech/docker-data-root-mount-race.md --- family/plans/kraken-backup-to-truenas.md | 76 ++++++- personal/tech/docker-data-root-mount-race.md | 203 ++++++------------- 2 files changed, 132 insertions(+), 147 deletions(-) diff --git a/family/plans/kraken-backup-to-truenas.md b/family/plans/kraken-backup-to-truenas.md index 57cdc8c7..2d14f1dc 100644 --- a/family/plans/kraken-backup-to-truenas.md +++ b/family/plans/kraken-backup-to-truenas.md @@ -1,8 +1,8 @@ # Kraken → TrueNAS — автобэкап стека > **Создано:** 2026-09-20 -> **Обновлено:** 2026-09-20 (реализация: шаги 1-6 ✅ и проверены живым прогоном; остался только шаг 7 — отключить старый Kraken→Mail.ru, ждёт апрува Alex) -> **Статус:** ✅ РАБОТАЕТ. Живой прогон прошёл end-to-end: архив **165 МБ / 36985 файлов** лёг на TrueNAS, владелец `nas:nas`, jellyfin и `backup.log` исключены. Крон `0 4 * * *` установлен. **Остаток: шаг 7.** +> **Обновлено:** 2026-09-20 (✅ ВСЕ 7 ШАГОВ ВЫПОЛНЕНЫ И ПРОВЕРЕНЫ) +> **Статус:** ✅ **РАБОТАЕТ END-TO-END.** Архив **165 МБ / 36985 файлов** лёг на TrueNAS (`nas:nas`), jellyfin и `backup.log` исключены. Крон `0 4 * * *` установлен. Старый бэкап Kraken→Mail.ru **отключён**. > **Задача:** бэкап стека Kraken складывать на TrueNAS в `/mnt/RED_2TB/backup/kraken/`, откуда он автоматически уезжает в Mail.ru Cloud существующим rclone-скриптом TrueNAS (**скрипт не трогаем**). ## ✅ СТАТУС РЕАЛИЗАЦИИ (2026-09-20) @@ -15,7 +15,7 @@ | 4 | Ротация 14 архивов | ✅ | Реализована: `ls -1t kraken-docker-*.tar.gz \| tail -n +15 \| rm` на стороне TrueNAS. В прогоне отработала (архив один → удалять нечего, шаг прошёл без ошибок). | | 5 | Крон `0 4 * * *` | ✅ | Поставлен: **`/etc/cron.d/kraken-backup`** (root:root 644), строка `0 4 * * * root /home/kraken/kraken-backup-to-truenas.sh`. `cron` активен. Не конфликтует с root-crontab (`sd-backup.sh` 03:00 вс, timemachine). | | 6 | Живой прогон | ✅ | **Проверено end-to-end:** tar 603M → 165 МБ, rsync 52 с, размер совпал (`165704366 == 165704366`), лог `=== KRAKEN BACKUP DONE ===`. На TrueNAS: `/mnt/RED_2TB/backup/kraken/kraken-docker-20260920-115925.tar.gz`, **36985 файлов**, владелец `nas:nas`. | -| 7 | Выключить старый Kraken→Mail.ru | ⏸ | **НЕ СДЕЛАНО** — ждёт апрува Alex. Бэкап снят: `/home/kraken/backups/rclone-disable-20260920-120606/` (`crontab.bak`, `backup.sh.bak`). Готовый новый crontab: `/Users/admin/kraken-recovery/rclone-crontab.new` (строка закомментирована). См. раздел «ШАГ 7» ниже. | +| 7 | Выключить старый Kraken→Mail.ru | ✅ | **СДЕЛАНО.** Строка `0 3 * * 0` в `/config/rclone/crontab` закомментирована, применено (`crontab /config/rclone/crontab`). Проверено: активных задач в контейнере **нет** (`crontab -l \| grep -v '^#'` пусто), `crond` жив, контейнер `Up`. Бэкап для отката: `/home/kraken/backups/rclone-disable-20260920-120606/`. | ## ✅ РЕШЕНИЕ БЛОКЕРА ПРАВ: вариант C — весь скрипт под `sudo` @@ -117,7 +117,75 @@ Mail.ru Cloud (зашифровано) | 4 | Ротация на стороне Kraken vs TrueNAS | Делать ротацию **на TrueNAS** (там архивы), rm старых | | 5 | `backup.log` 65 МБ растёт | Исключить из tar (`--exclude`) + рассмотреть ротацию самого лога | -## ⏸ ШАГ 7 — выключить старый бэкап Kraken→Mail.ru (ЖДЁТ АПРУВА) +## ✅ ШАГ 7 — ВЫПОЛНЕН (2026-09-20) + +**Сделано:** строка `0 3 * * 0 /config/rclone/backup.sh ...` в `/config/rclone/crontab` контейнера `rclone` **закомментирована** (`#0 3 * * 0 ...`), применено через `crontab /config/rclone/crontab`. + +**Проверено:** +- `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 +` → **пусто** (активных задач нет) +- Контейнер `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. + +## ⏸ ШАГ 7 — ИСТОРИЯ (выполнен, оставлено для контекста) **Почему обязателен:** контейнер `rclone` на Kraken сейчас льёт `docker` напрямую в `mailru-crypt:kraken/docker` (вс 03:00). После перевода на TrueNAS это надо выключить — иначе двойная нагрузка и тот же 507 (место на Mail.ru переполнено). diff --git a/personal/tech/docker-data-root-mount-race.md b/personal/tech/docker-data-root-mount-race.md index 662e910f..24083390 100644 --- a/personal/tech/docker-data-root-mount-race.md +++ b/personal/tech/docker-data-root-mount-race.md @@ -1,166 +1,83 @@ ---- -title: Docker data-root ↔ mount race — диагностика -created: '2026-09-20' -type: tech -namespace: personal -tags: - - docker - - systemd - - mount - - kraken - - diagnostics -confidence: high -related: - - '[[family/how-to/kraken-access]]' - - '[[personal/tech/xray-reverse-tunnel-kraken-truenas]]' ---- +# Docker data-root ↔ монтирование диска — гонка (mount race) -# Docker data-root ↔ mount race +> **Обнаружено:** 2026-09-20 на Kraken (RPi5/OMV, USB-HDD). +> **Симптом-класс:** «пропали все контейнеры», `docker ps -a` пусто, `docker info` = `Containers=0` — **при целых данных на диске**. +> **Связанные:** [[family/how-to/kraken-access]], [[personal/tech/xray-reverse-tunnel-kraken-truenas]], [[family/plans/kraken-backup-to-truenas]] -> Обнаружено 2026-09-20 на Kraken (RPi5/OMV, USB-HDD). Зеркало скилла -> `docker-data-root-mount-race` (категория `docker-service-administration`). +## Механизм (главное) -## Триггер +`docker.service` **не имеет зависимости от монтирования диска** (`RequiresMountsFor=` пусто). Если `dockerd` стартует **раньше**, чем монтируется диск с `data-root`: -Хост, у которого Docker `data-root` лежит на **диске, монтирующемся поздно** -(USB-HDD, OMV `/srv/dev-disk-by-uuid-*`, RPi). Признаки: +1. dockerd открывает путь `data-root` → диска там ещё нет → видит **пустой** каталог на root-ФС (или создаёт новый). +2. Через N секунд диск монтируется **поверх** точки монтирования — реальные данные «появляются». +3. Но dockerd уже разобрал своё (пустое) состояние и держит его **в памяти**. Перечитывания нет. -- `docker ps -a` **пусто**, но контейнеры раньше были. -- `docker info` → `Containers=0`, при этом диск с `docker-data` смонтирован. -- После reboot сервисы в контейнерах (cloudflared, reverse-proxy bridge, jellyfin…) - молча «down» — но контейнеры **не удалялись**. -- journal: `Duplicate entry in '/etc/fstab'?` от `systemd-fstab-generator`. +Итог: **контейнеры целы на диске, но daemon их не видит.** Ничего не потеряно. -## Механизм - -`dockerd` стартует **раньше**, чем монтируется диск с его `data-root`. Открывает -путь, видит пусто → инициализирует **пустое** состояние (0 контейнеров). Через -секунды диск монтируется поверх точки, реальный `containers/` появляется — но -dockerd уже разобрал состояние и **не перечитывает**. - -**Ничего не потеряно.** Metadata цела на диске, daemon её просто не видит. - -Причина: у `docker.service` **нет `RequiresMountsFor=`** на маунт-юнит диска. - -## Диагностика +## Как распознать (диагностика) ```bash -# 1) daemon думает, что пусто -docker ps -a --format "{{.Names}}\t{{.Status}}" # пусто +# 1) daemon видит 0, а на диске metadata есть? docker info --format "{{.DockerRootDir}} | containers={{.Containers}}" +sudo ls /srv/dev-disk-by-uuid-<...>/docker-data/containers/ | wc -l # 35 = целы -# 2) но metadata НА ДИСКЕ (нужен root; каталог 711 root:root) -sudo ls -la /srv/dev-disk-by-uuid-/docker-data/ -sudo ls /srv/dev-disk-by-uuid-/docker-data/containers/ | wc -l # >0 = целы +# 2) реальное время старта dockerd (ExecMainStartTimestamp ВРЁТ — stale!) +ps -o pid,lstart,cmd -p $(pgrep -x dockerd) -# 3) подтвердить гонку: dockerd стартовал РАНЬШЕ диска -ps -o pid,lstart,cmd -p "$(pgrep -x dockerd)" # истинное время старта -uptime -s # время boot -sudo dmesg | grep -iE "EXT4-fs \(sd[a-z]" | tail -5 # когда ФС смонтировалась -findmnt /srv/dev-disk-by-uuid- +# 3) когда смонтирован диск (dmesg: "[130s] EXT4-fs (sda1): mounted") +uptime -s +findmnt /srv/dev-disk-by-uuid-<...> +sudo journalctl -u docker -b | head -5 -# 4) подтвердить отсутствие зависимости (корень) -systemctl show docker -p RequiresMountsFor # пусто = нет порядка -grep -c "" /etc/fstab # 2 = дубль +# 4) есть ли зависимость от mount? +systemctl show docker -p RequiresMountsFor # пусто = ГОНКА ВОЗМОЖНА ``` -**ПИТФОЛЛ — не то поле времени.** `systemctl show docker -p ExecMainStartTimestamp` -может вернуть **stale** значение (прошлая активация). Реальное время — -`ps -o lstart -p $(pgrep -x dockerd)`. +**Ключевой признак:** `docker ps -a` пусто + `docker-data/containers/` полон → **это гонка**, а не потеря данных. -**ПИТФОЛЛ — не тот источник RestartPolicy.** `RestartPolicy.Name` из -`docker-data/containers//hostconfig.json` показывает то, что передал -*исходный* `docker run` — часто `null`, даже когда политика есть в живом daemon -state. НЕ делать вывод «контейнеры не поднимутся сами». Проверять живой daemon: -```bash -docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' -``` -Если `unless-stopped` — контейнеры **сами** вернутся при рестарте daemon, ручной -`docker start` не нужен. +> ⚠️ **Питфолл:** `systemctl show docker -p ExecMainStartTimestamp` дал stale-время (`15:18:36`), истинное — из `ps -o lstart` (`17:31:01`). Сверять с `dmesg` по времени монтирования. + +## Факт инцидента 2026-09-20 (Kraken) + +| Событие | Время | +|---|---| +| Boot | 17:29:17 | +| Ядро видит `sda1` | +130 с → 17:31:27 | +| **dockerd стартовал** | **17:31:01** ← на 26 с раньше диска | +| Диск смонтирован | 17:31:27 | + +USB-HDD на Kraken поднимается **~130 с** — dockerd его обгоняет. ## Фикс -### Немедленный (вернуть сервисы — диск уже смонтирован) +### 1. Немедленно (диск уже смонтирован) ```bash -sudo systemctl restart docker -docker info --format "containers={{.Containers}} running={{.ContainersRunning}}" -``` -Контейнеры с `restart: unless-stopped` поднимаются сами. - -### Постоянный (сделать reboot безопасным) -1. Бэкап: `/etc/fstab`, `/etc/docker/daemon.json`, compose-каталоги сервисов → - датированная папка. -2. Drop-in, чтобы systemd не стартовал dockerd раньше диска: - `/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 - ``` - Имя `.mount` юнита = путь, где `/` → `-`, а каждый литеральный `-` escaped как - `\x2d` (systemd escaping). -3. `sudo systemctl daemon-reload` -4. Убрать **дубль** строки монтирования того же диска из `/etc/fstab` (systemd - отказывается от дублей; OMV часто добавляет свою рядом с ручной `UUID=...`). -5. Проверка: - ```bash - findmnt --verify --fstab # rc=0 - systemctl show docker -p Requires -p After # маунт-юнит в ОБОИХ - sudo systemctl restart docker # sanity: контейнеры перезагрузятся - ``` - -## Нельзя - -- Не предполагать потерю данных, если `docker ps` пусто, но диск смонтирован — - **сначала посчитать `docker-data/containers/`**. `Containers=0` в `docker info` - не доказательство удаления. -- Не `rm -rf` «устаревший» docker-data — это и есть данные. -- Не править `/etc/fstab` через sed; писать полный файл локально, валидировать - `findmnt --verify --fstab`, затем копировать на место. - -## Чеклист после фикса - -- [ ] `docker info` count == число каталогов в `docker-data/containers/`. -- [ ] `systemctl show docker -p RequiresMountsFor` **не пусто** и указывает на `docker-data`. -- [ ] `grep -c "" /etc/fstab` = `1`. -- [ ] После тестового `systemctl restart docker` критичные контейнеры `Up`. -- [ ] При следующем плановом reboot: `docker ps -q | wc -l` возвращает ожидаемое число, не `0`. - -## Применение на Kraken (2026-09-20) - -Полный кейс: [[family/how-to/kraken-access]] и -[[personal/tech/xray-reverse-tunnel-kraken-truenas]]. -Кратко: boot 17:29:17, dockerd 17:31:01, диск 17:31:27 (на 26 с позже) → 0 -контейнеров. Фикс: drop-in `RequiresMountsFor=/srv/.../6194539b.../docker-data` -+ чистка fstab. Бэкап: `/home/kraken/backups/docker-recovery-20260920-105228/`. - -## Смежная ловушка: смешанные владельцы под data-root - -Бэкап (или tar) дерева сервисов на том же диске часто упирается в **разных владельцев** — -часть каталогов `root:root` (созданы root-контейнерами), часть `1000:1000`. -Бэкап от обычного юзера получит `Permission denied` на root-каталогах и **молча -сделает неполный архив**. - -```bash -sudo ls -ldn /srv/dev-disk-by-uuid-/docker/*/ | awk '{print $3, $4, $NF}' +sudo systemctl restart docker # daemon перечитает диск → все контейнеры вернутся ``` -Если `sudo -n true` проходит (NOPASSWD есть) — архивировать под `sudo -n`, а на приёмной -стороне выставить владельца бэкап-юзера: `rsync --chown=:` (не голое копирование, -иначе файл ляжет `root:root`). - -**ПИТФОЛЛ — длинный `chown -R`/архив умирает вместе с SSH-сессией.** Рекурсивный `chown` -(или большой `tar`) по медленному USB-HDD идёт минутами и убивается при обрыве SSH. -Запускать отвязанно: -```bash -sudo systemd-run --unit= --collect --property=Type=oneshot chown -R 1000:1000 -systemctl is-active # 'activating' = ещё идёт, доживёт до конца независимо от SSH +### 2. Постоянно, reboot-безопасно — 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\x2d....mount ``` -Тем же приёмом — любую долгую удалённую задачу через флаки-линк. +Затем `sudo systemctl daemon-reload`. Проверка: `systemctl show docker -p Requires -p After` → маунт-юнит в **обоих**. -**ПИТФОЛЛ — контейнер в `Restarting (1)` с `mkdir: Permission denied` на своём каталоге данных** — -тот же класс багов, но наоборот: UID владельца на хосте не совпадает с UID, под которым -работает контейнер (образ `USER` или compose `user:`). Лечится `chown : `. -Ожидаемый UID: `docker inspect --format '{{.Config.User}}'`. -Первые несколько `mkdir`-ошибок могут быть **остатком буфера** от прежнего падения — прежде -чем объявлять провал, убедиться, что контейнер реально держится `Up`. +> Имя маунт-юнита: systemd экранирует `-` как `\x2d`. Взять из `systemctl list-units --type=mount`. + +### 3. Проверить fstab на дубли +Симптом в journal: `Failed to create unit file ... already exists. Duplicate entry in '/etc/fstab'?` +На Kraken было **два** маунта одного диска (OMV-блок + ручная строка `UUID=...`) → неопределённость порядка. Убрать дубль. + +## Сопутствующий питфолл: права на `~` контейнера + +После перерегистрации диска каталог может получить **чужой UID**. На Kraken `docker/hermes/config` стал `10000:10000 700`, а контейнер работает под `USER 1000` → `mkdir: /opt/data: Permission denied` → **crash-loop** (`Restarting (1)`). + +**Признак:** `Restarting (1)` + в логах циклический `Permission denied` на `HOME`-каталог. +**Фикс:** `sudo chown -R 1000:1000 ` (UID взять из `docker inspect --format '{{.Config.User}}'`). +**Как запускать долгий chown:** `sudo -n systemd-run --unit= --collect --property=Type=oneshot chown -R ...` — переживает разрыв SSH (большое дерево на USB-диске chown'ится минутами). + +## Урок + +Любой сервис, зависящий от данных на **медленно монтирующемся диске** (USB-HDD), ОБЯЗАН иметь `RequiresMountsFor`. Без него — гонка при каждом reboot, лотерея.