129 lines
10 KiB
Markdown
129 lines
10 KiB
Markdown
# 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]]
|
||
|
||
## Механизм (главное)
|
||
|
||
`docker.service` **не имеет зависимости от монтирования диска** (`RequiresMountsFor=` пусто). Если `dockerd` стартует **раньше**, чем монтируется диск с `data-root`:
|
||
|
||
1. dockerd открывает путь `data-root` → диска там ещё нет → видит **пустой** каталог на root-ФС (или создаёт новый).
|
||
2. Через N секунд диск монтируется **поверх** точки монтирования — реальные данные «появляются».
|
||
3. Но dockerd уже разобрал своё (пустое) состояние и держит его **в памяти**. Перечитывания нет.
|
||
|
||
Итог: **контейнеры целы на диске, но daemon их не видит.** Ничего не потеряно.
|
||
|
||
## Как распознать (диагностика)
|
||
|
||
```bash
|
||
# 1) daemon видит 0, а на диске metadata есть?
|
||
docker info --format "{{.DockerRootDir}} | containers={{.Containers}}"
|
||
sudo ls /srv/dev-disk-by-uuid-<...>/docker-data/containers/ | wc -l # 35 = целы
|
||
|
||
# 2) реальное время старта dockerd (ExecMainStartTimestamp ВРЁТ — stale!)
|
||
ps -o pid,lstart,cmd -p $(pgrep -x dockerd)
|
||
|
||
# 3) когда смонтирован диск (dmesg: "[130s] EXT4-fs (sda1): mounted")
|
||
uptime -s
|
||
findmnt /srv/dev-disk-by-uuid-<...>
|
||
sudo journalctl -u docker -b | head -5
|
||
|
||
# 4) есть ли зависимость от mount?
|
||
systemctl show docker -p RequiresMountsFor # пусто = ГОНКА ВОЗМОЖНА
|
||
```
|
||
|
||
**Ключевой признак:** `docker ps -a` пусто + `docker-data/containers/` полон → **это гонка**, а не потеря данных.
|
||
|
||
> ⚠️ **Питфолл:** `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 # daemon перечитает диск → все контейнеры вернутся
|
||
```
|
||
|
||
### 2. Постоянно, reboot-безопасно — drop-in
|
||
`/etc/systemd/system/docker.service.d/mounts.conf`:
|
||
```ini
|
||
[Unit]
|
||
RequiresMountsFor=/srv/dev-disk-by-uuid-<UUID>/docker-data
|
||
After=srv-dev\x2ddisk\x2dby\x2duuid\x2d<UUID>\x2d....mount
|
||
```
|
||
Затем `sudo systemctl daemon-reload`. Проверка: `systemctl show docker -p Requires -p After` → маунт-юнит в **обоих**.
|
||
|
||
> Имя маунт-юнита: 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 <dir>` (UID взять из `docker inspect --format '{{.Config.User}}'`).
|
||
**Как запускать долгий chown:** `sudo -n systemd-run --unit=<name> --collect --property=Type=oneshot chown -R ...` — переживает разрыв SSH (большое дерево на USB-диске chown'ится минутами).
|
||
|
||
## Урок
|
||
|
||
Любой сервис, зависящий от данных на **медленно монтирующемся диске** (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 поднялся, но падает внутри (своя ошибка приложения).
|
||
|
||
> ℹ️ **Обновление 2026-09-20 (позже в тот же день):** `radarr` был **пересоздан** (переведён из общего проекта `docker` в свой `docker/radarr`, обновлён до 6.4.4) — см. [[family/how-to/arr-stack-kraken]]. `watchtower` сменил образ на `nickfedor/watchtower:1.22.2` — см. [[family/how-to/kraken-watchtower]]. Число контейнеров и их состав могли измениться; проверка `docker ps -q | wc -l` по-прежнему корректна как индикатор гонки, но ожидаемое число сверять заново.
|
||
|
||
## Крон не догоняет пропущенное окно
|
||
|
||
Общий питфолл для сервисов на этом узле: если Kraken лежал, cron-задачи (бэкап `0 4 * * *`, t610-подобные) **пропускаются без компенсации**. Для критичных — добавлять `@reboot`-строку с проверкой «артефакт за сегодня уже есть?».
|
||
|
||
## 🔴 Диск НЕ нестабилен — «нестабильность» была ошибочным диагнозом (2026-09-25)
|
||
|
||
Формулировку **«USB-HDD на Kraken нестабилен»** нельзя использовать: это обобщение разового инцидента
|
||
2026-09-01 (циклы USB enumeration: `device descriptor read/64 error -110`, `device not accepting address
|
||
error -62`, `[sda] Read Capacity(10) failed` → `0 B`), который позже был **объяснён и закрыт**.
|
||
|
||
**Истинная причина всех симптомов «диск отвалился» — гонка монтирования** (этот документ, раздел выше):
|
||
диск монтируется ~130 с, `dockerd` стартует раньше и видит пустой `data-root`. Фикс
|
||
(`RequiresMountsFor` drop-in) применён 2026-09-20 и **проверен живым reboot'ом** — все контейнеры
|
||
поднялись сами, ручных `docker start` не потребовалось.
|
||
|
||
**Реальные характеристики диска (не дефекты):**
|
||
- 5400rpm 2.5" механика → ~95 МБ/с на чтение. Это лимит механики, не USB (линк USB3 = 5000 Mbps подтверждён).
|
||
- Медленный подъём при boot (~130 с) — свойство, а не поломка; обрабатывается зависимостью в systemd.
|
||
|
||
**Правило для будущих сессий:** при «контейнеры пропали / сервис не поднялся после reboot» — проверять
|
||
гонку (`docker info` vs содержимое `docker-data/containers/`), а не списывать на «нестабильный диск».
|
||
Не давать оценку «диск нестабилен» без новых доказательств (raw USB-ошибок в `dmesg` за пределами boot).
|
||
|
||
## Связанные заметки
|
||
|
||
- [[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-туннеля от той же гонки
|