104 lines
7.3 KiB
Markdown
Executable File
104 lines
7.3 KiB
Markdown
Executable File
# 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 <data-root>/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 "<DISK-UUID>" /etc/fstab # 2 = дубль
|
||
```
|
||
|
||
**Питфоллы диагностики:**
|
||
- `systemctl show docker -p ExecMainStartTimestamp` даёт **stale** время → только `ps -o lstart`.
|
||
- `RestartPolicy.Name` в `hostconfig.json` = `null`, но в live-состоянии политика есть. Проверять `docker inspect <name> --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-<DISK>/docker-data
|
||
After=srv-dev\x2ddisk\x2dby\x2duuid\x2d<DISK>.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 <image>:latest -c '<bin> --version'`. Если версия БД выше версии сервера → **пересоздать контейнер на верном образе**, НЕ даунгрейдить данные.
|
||
|
||
## Смежная ловушка: смешанные владельцы под data-root
|
||
|
||
Бэкап/tar дерева сервисов на том же диске: часть каталогов `root:root`, часть `1000:1000` → обычный юзер получает `Permission denied` и **молча частичный архив**.
|
||
|
||
```bash
|
||
sudo ls -ldn <data-root>/../docker/*/ | awk '{print $3, $4, $NF}' # владельцы
|
||
```
|
||
Если `sudo -n true` проходит — архивировать под `sudo -n`, а на приёмнике ставить владельца: `rsync --chown=<user>:<group>`.
|
||
|
||
**Питфолл — долгий `chown -R`/tar умирает с SSH-сессией.** Запускать отсоединённо:
|
||
```bash
|
||
sudo systemd-run --unit=<name> --collect --property=Type=oneshot chown -R 1000:1000 <path>
|
||
```
|
||
|
||
**Питфолл — `Restarting (1)` + `mkdir: Permission denied`** на data-каталоге: владелец на хосте ≠ UID контейнера. `docker inspect <name> --format '{{.Config.User}}'` → `chown <uid>:<gid>`. Первые `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 "<UUID>" /etc/fstab` == 1
|
||
- [ ] После `systemctl restart docker` критичные контейнеры `Up`
|
||
- [ ] После реального reboot: `docker ps -q | wc -l` == ожидаемое, не 0
|