[2026-09-20] eagle: family/plans/kraken-backup-to-truenas.md personal/tech/docker-data-root-mount-race.md
This commit is contained in:
1 parent
6cb187cee5
commit
4101de5ebc
2 files changed
+132
-147
No files matched your search
@@ -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-<DISK>/docker-data/
|
||||
sudo ls /srv/dev-disk-by-uuid-<DISK>/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-<DISK>
|
||||
# 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 "<DISK-UUID>" /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/<id>/hostconfig.json` показывает то, что передал
|
||||
*исходный* `docker run` — часто `null`, даже когда политика есть в живом daemon
|
||||
state. НЕ делать вывод «контейнеры не поднимутся сами». Проверять живой daemon:
|
||||
```bash
|
||||
docker inspect <name> --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-<DISK>/docker-data
|
||||
After=srv-dev\x2ddisk\x2dby\x2duuid\x2d<DISK>.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 "<DISK-UUID>" /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-<DISK>/docker/*/ | awk '{print $3, $4, $NF}'
|
||||
sudo systemctl restart docker # daemon перечитает диск → все контейнеры вернутся
|
||||
```
|
||||
|
||||
Если `sudo -n true` проходит (NOPASSWD есть) — архивировать под `sudo -n`, а на приёмной
|
||||
стороне выставить владельца бэкап-юзера: `rsync --chown=<user>:<group>` (не голое копирование,
|
||||
иначе файл ляжет `root:root`).
|
||||
|
||||
**ПИТФОЛЛ — длинный `chown -R`/архив умирает вместе с SSH-сессией.** Рекурсивный `chown`
|
||||
(или большой `tar`) по медленному USB-HDD идёт минутами и убивается при обрыве SSH.
|
||||
Запускать отвязанно:
|
||||
```bash
|
||||
sudo systemd-run --unit=<name> --collect --property=Type=oneshot chown -R 1000:1000 <path>
|
||||
systemctl is-active <name> # 'activating' = ещё идёт, доживёт до конца независимо от SSH
|
||||
### 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` → маунт-юнит в **обоих**.
|
||||
|
||||
**ПИТФОЛЛ — контейнер в `Restarting (1)` с `mkdir: Permission denied` на своём каталоге данных** —
|
||||
тот же класс багов, но наоборот: UID владельца на хосте не совпадает с UID, под которым
|
||||
работает контейнер (образ `USER` или compose `user:`). Лечится `chown <container-uid>:<gid> <host-dir>`.
|
||||
Ожидаемый UID: `docker inspect <name> --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 <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, лотерея.
|
||||
Reference in new issue
Block a user