Томи Docker: bind-маунти проти іменованих томів — що краще переживе міграцію

Було корисно?

Ви не усвідомлюєте, скільки у вашому застосунку — «просто файли», поки не спробуєте його перенести. Хост помирає, команда хмарної інфраструктури «допоміжно»
замінює інстанси, або ви вирішуєте змінити дистрибутив через CVE, що зруйнував ваші плани на вихідні. Раптом контейнери стають переносними, а ваші дані — ні.

Bind-маунти й іменовані томи обидва виглядають як «персистентне сховище» у Docker. Вони не еквівалентні. Один мігрується як звичайна файловa система.
Інший мігрується як внутрішня деталь реалізації Docker. Помилковий вибір — і міграція працює, поки вам не знадобиться вона по-справжньому.

Думка: що краще переживе міграцію

Якщо вашим головним пріоритетом є «я можу підняти сховище з цього хоста й переселити його з мінімальними проблемами»,
bind-маунти краще переживають міграціїза умови, що ви контролюєте розкладку файлової системи хоста.
Вони явні, видимі й переносні тими самими інструментами, яким ви вже довіряєте для руху даних: rsync, знімки, резервні копії,
реплікація, відновлення, контрольні суми. Ви можете про них міркувати без звернення до Docker.

Якщо вашим пріоритетом є «хочу, щоб Docker керував розташуванням і життєвим циклом сховища, і я готовий ставитися до цього як до артефакту застосунку»,
іменовані томи підходять — часто вони полегшують експлуатацію в межах одного хоста.
Вони зменшують людські помилки на кшталт помилок у шляхах і виглядають чистіше в Compose-стеках. Але вони не мігрують автоматично.
Треба свідомо експортувати/імпортувати їх.

Мій типовий підхід у продакшні нудний:

  • Бази даних і stateful-сервіси: bind-маунт до директорії хоста, яка лежить на реальному сховищі,
    яке ви вже бекапите (LVM, ZFS dataset, EBS volume, SAN LUN або те, що ваша організація називає «storage»).
  • Рівень застосунку з відновлюваними даними: іменовані томи прийнятні, іноді навіть кращі.
  • Усе, що може знадобитися відновити під тиском: уникайте «тільки Docker» сховищ, якщо ви не репетирували експорт томів.

Ще одна думка: міграції ламаються тому, що люди плутають «переносність контейнера» з «переносністю даних».
Контейнер — це легка частина. Байти — це робота.

Що фактично відбувається на диску

Bind-маунти: Docker як гість, ваша файловa система — джерело істини

Bind-маунт означає: «Візьміть цей /path/on/host і покажіть його в /path/in/container
Docker не володіє даними. Docker навіть не намагається прикидатися. Шлях на хості існує, застосовуються права доступу, політики SELinux/AppArmor
застосовуються, і правила знімків або реплікації вашої команди сховища застосовуються.

Наслідок для міграції: якщо ви можете перемістити цей шлях на хості — перемістивши диск, знявши снапшот dataset’у, відновивши резервну копію
або rsync’ом — ви можете перемістити дані. Docker не додає спеціального формату.

Іменовані томи: Docker як орендодавець (а ви винаймаєте місце)

Іменований том означає: «Я виділю сховище для цього тому, кероване драйвером томів.»
Зазвичай драйвер — local. На Linux з дефолтним движком це часто означає, що Docker зберігає дані тому під:
/var/lib/docker/volumes/<volume>/_data.

Цей шлях не є стабільним API. Це реалізаційна деталь, яка змінюється залежно від:
драйвера сховища, rootless-режиму, VM-шару Docker Desktop, упаковки дистрибутива та політик безпеки, які можуть з’явитися наступного кварталу.

Наслідок для міграції: щоб безпечно перемістити іменовані томи, поводьтеся з ними як із керованим Docker-ом ресурсом:
inspect, export, move, import, verify. Не просто «копіюйте /var/lib/docker», якщо ви не тестували ту саму версію движка,
файлову систему та комбінацію драйвера. Іноді це працює. Іноді — поки не зламається.

Жарт №1: Docker томи схожі на офісні рослини: ніхто не пам’ятає, хто їх власник, поки не треба переносити офіс.

Факти й історичний контекст (чому сьогодні такий безлад)

  1. Спочатку Docker заохочував епемерні контейнери (культура 2013 року): «перебудуй, не патчуй.»
    Персистентні дані навмисно виносилися за межі контейнера, тому томи виглядали прилаштованими, а не рідними.
  2. Іменовані томи були виправленням зручності для «маунть усе», що ускладнювало Compose-файли й було крихким між хостами
    з різними директоріями.
  3. Драйвер local не є універсальною угодою. На Linux він відображається в файлову систему хоста;
    у Docker Desktop — у файлову систему VM, якою ви не керуєте так само безпосередньо.
  4. Драйвери зберігання змінювалися з часом (AUFS → overlay2 став поширеним). Поведінка «копіювання директорії даних Docker»
    тісно пов’язана з форматом на диску драйвера зберігання.
  5. Compose v2 зробив іменовані томи типовими, бо це зручно і уникає припущень про шляхи.
    Зручність — це спосіб створити технічний борг з усмішкою.
  6. Rootless Docker і user namespaces перетворили проблеми з правами томів із «рідкісного краю» на «рутинний сюрприз»,
    особливо з bind-маунтами, де важливе відображення UID/GID.
  7. Маркування SELinux давно є підводним каменем при міграціях. Перенесення bind-маунтованих даних між хостами з різними контекстами SELinux
    може зламати робочі навантаження, навіть якщо байти коректні.
  8. Kubernetes нормалізував ідею явних персистентних томів з драйверами й заявками (claims). Іменовані томи Docker виглядають подібно,
    але це не та сама історія міграції або рівень абстракції.

Матриця виживання при міграції: bind-маунти vs іменовані томи

Що насправді означає «пережити міграцію»

«Пережити» тут значить багато. Міграція вважається успішною, коли:

  • Дані прибувають цілісними (контрольні суми співпадають, а не лише «воно стартує»).
  • Права доступу та власність коректні для моделі виконання контейнера.
  • Характеристики продуктивності не катастрофічно відрізняються (IOPS, fsync-латентність, поведінка кешу сторінок).
  • Операційні робочі процеси все ще працюють (бекапи, відновлення, налагодження інцидентів).

Bind-маунти: сильні сторони та режими відмов

Сильні сторони:

  • Прямо мігруються звичайними інструментами.
  • Прозорі: ви можете бачити дані без запущеного Docker.
  • Сумісні з можливостями сховища хоста: знімки, реплікація, квоти, шифрування, дедуплікація, стиснення.
  • Краще для комплаєнсу, коли аудитори питають «Де дані?» і ви хочете відповісти без лекції про Docker.

Режими відмов:

  • Зв’язування зі шляхом: ваш стек передбачає /srv/app/db на кожному хості.
    Якщо ви замінили хост і забули ту директорію (або її маунт), контейнер стартує на порожній директорії. Вітаємо, ви створили втрату даних.
  • Несумісність прав: контейнерний користувач очікує UID 999, а відновлені файли належать 1001.
    «Permission denied» — це ввічлива версія цієї помилки.
  • SELinux/AppArmor: байти є, але доступ блокується. Це найнеприємніша правильна поведінка в Linux.
  • Небажане розкриття хоста: bind-маунт / або /var/run/docker.sock — це інцидент безпеки в очікуванні.

Іменовані томи: сильні сторони та режими відмов

Сильні сторони:

  • Портативність конфігурації: Compose-файли не хардкодять шляхи хоста.
  • Безпечніші за замовчуванням: менше катастроф через «я помилився в шляху».
  • Ізоляція: дані під Docker-керуванням, менше випадкових маніпуляцій.
  • Гнучкість драйверів: плагіни дозволяють вказати NFS, блокові пристрої або хмарне сховище (але тепер треба підтримувати драйвер).

Режими відмов:

  • Міграція вимагає кроків, що знають про Docker. Якщо ви трактуєте дані тому як непрозорий бінарник під /var/lib/docker,
    ви успадковуєте всю внутрішню складність Docker.
  • Прихована залежність від налаштування движка: rootless, VM Desktop, драйвер зберігання, вибір файлової системи.
  • Сліпі зони в бекапах: інструменти інфраструктури часто виключають /var/lib/docker, бо вважають його «відновлюваним».
    Ваші іменовані томи можуть жити в зоні, що не охоплена бекапом.

Отже, що краще переживе міграцію?

У міграціях явне перемагає неявне. Bind-маунти явні: ви бачите їх і можете перемістити.
Іменовані томи можуть пережити міграцію так само добре якщо ви реалізуєте експорт/імпорт і не покладаєтесь на недокументовані шляхи.
Більшість команд цього не репетирують. Вони репетирують «пере-розгорнути контейнери», а не «переселити стан».

Практичні завдання (команди, виводи й рішення)

Тут ви припиняєте здогадки. Кожне завдання включає: команду, що означає вивід і яке рішення ви приймаєте.
Запустіть їх на вихідному хості перед міграцією і знову на хості-призначенні після.

Завдання 1: Перелік контейнерів і їхніх маунтів (знайдіть, що насправді stateful)

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Mounts}}'
NAMES          IMAGE                 MOUNTS
pg01           postgres:16           pgdata
api01          myorg/api:2.4.1       /srv/api/config:/app/config
grafana01      grafana/grafana:11    grafana-storage

Значення: колонка mounts змішує іменовані томи (наприклад, pgdata) та bind-маунти (шляхи хоста показуються як host:container).

Рішення: усе, що містить бізнес-стан, отримує план міграції. «Це лише кеш» — це не план.

Завдання 2: Інспект контейнера для його маунтів (правда, не відчуття)

cr0x@server:~$ docker inspect pg01 --format '{{json .Mounts}}'
[{"Type":"volume","Name":"pgdata","Source":"/var/lib/docker/volumes/pgdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]

Значення: Type показує bind чи volume. Source для локального іменованого тому часто вказує під директорією даних Docker.

Рішення: якщо це іменований том — плануйте експорт/імпорт. Якщо це bind-маунт — плануйте міграцію файлової системи й перевірку прав.

Завдання 3: Інспект іменованого тому (драйвер, mountpoint, мітки)

cr0x@server:~$ docker volume inspect pgdata
[
  {
    "CreatedAt": "2025-11-02T09:14:21Z",
    "Driver": "local",
    "Labels": {
      "com.docker.compose.project": "billing",
      "com.docker.compose.volume": "pgdata"
    },
    "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
    "Name": "pgdata",
    "Options": null,
    "Scope": "local"
  }
]

Значення: Scope: local означає «це не автоматично шариться». Mountpoint — де байти живуть на цьому хості.

Рішення: якщо ви очікували HA-сховище — його немає. Якщо ви очікували «авто-міграцію» — її немає теж.

Завдання 4: Підтвердьте, що шлях хоста за bind-маунтом існує і змонтований коректно

cr0x@server:~$ ls -ld /srv/api/config
drwxr-x--- 2 root api 4096 Jan  2 09:10 /srv/api/config

cr0x@server:~$ findmnt -T /srv/api/config
TARGET SOURCE          FSTYPE OPTIONS
/srv   /dev/nvme0n1p2  ext4   rw,relatime

Значення: дані bind-маунта живуть на тій файловій системі, яка підтримує цей шлях; тут це ext4 на локальній NVMe-партіції.

Рішення: якщо findmnt показує неправильне джерело (або нічого), виправте маунти перед міграцією. Інакше ви перемістите порожню директорію-заповнювач.

Завдання 5: Перевірте вільне місце та inode-и (міграції ламаються через брак «не місця»)

cr0x@server:~$ df -h /srv /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  200G  120G   71G  63% /srv
/dev/nvme0n1p3  100G   92G  3.5G  97% /var/lib/docker

cr0x@server:~$ df -i /var/lib/docker
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/nvme0n1p3  655360 621104  34256   95% /var/lib/docker

Значення: розділ даних Docker майже повний і голодний по inode-ам. Це класична «щось починає поводитися дивно» конфігурація.

Рішення: не мігруйте безлад до нового хоста. Виправте ємність спочатку або змініть, де Docker зберігає дані.

Завдання 6: Визначте корінь даних Docker (не припускайте /var/lib/docker)

cr0x@server:~$ docker info --format '{{.DockerRootDir}}'
/var/lib/docker

Значення: тут Docker зберігає образи, шари і локальні іменовані томи (якщо драйвер не змінює це).

Рішення: для іменованих томів ви експортуєте звідси; для bind-маунтів це здебільшого неактуально, крім стратегій міграції движка.

Завдання 7: Знайдіть, які томи використовуються, а які сироти (звужуйте обсяг міграції)

cr0x@server:~$ docker volume ls
DRIVER    VOLUME NAME
local     billing_pgdata
local     grafana-storage
local     old_tmpdata

cr0x@server:~$ docker ps -a --format '{{.Names}}' | wc -l
7

Значення: список томів включає сирітські томи. Кількість контейнерів підказує, скільки робочих навантажень може посилатися на них.

Рішення: ідентифікуйте сироти перед міграцією; інакше ви сумлінно понесете мертві байти як сімейну реліквію, яку ніхто не хотів.

Завдання 8: Зіставте іменований том із контейнерами, що його використовують

cr0x@server:~$ docker ps -a --filter volume=billing_pgdata --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
NAMES   STATUS         IMAGE
pg01    Up 3 hours     postgres:16

Значення: лише pg01 використовує цей том.

Рішення: план міграції може бути по томах; вам не потрібно експортувати все «на всяк випадок».

Завдання 9: Експортуйте іменований том у tar‑архів (портативно й нудно)

cr0x@server:~$ docker run --rm -v billing_pgdata:/data -v /backup:/backup alpine:3.20 sh -c 'cd /data && tar -cpf /backup/billing_pgdata.tar .'
cr0x@server:~$ ls -lh /backup/billing_pgdata.tar
-rw-r--r-- 1 root root 2.1G Jan  2 10:01 /backup/billing_pgdata.tar

Значення: у вас є один артефакт, що репрезентує вміст тому.

Рішення: це безпечний дефолт для міграцій іменованих томів. Якщо tar-архів величезний — розгляньте простій чи інкрементальні стратегії.

Завдання 10: Імпортуйте tar‑архів іменованого тому на новому хості (і перевірте, що він не порожній)

cr0x@server:~$ docker volume create billing_pgdata
billing_pgdata

cr0x@server:~$ docker run --rm -v billing_pgdata:/data -v /backup:/backup alpine:3.20 sh -c 'cd /data && tar -xpf /backup/billing_pgdata.tar'

cr0x@server:~$ docker run --rm -v billing_pgdata:/data alpine:3.20 sh -c 'ls -la /data | head'
total 72
drwx------    19 999      999           4096 Jan  2 10:05 .
drwxr-xr-x     1 root     root          4096 Jan  2 10:05 ..
drwx------     5 999      999           4096 Jan  2 10:05 base
-rw-------     1 999      999             88 Jan  2 10:05 postmaster.opts

Значення: файли існують, і власність схожа на типовий Postgres (UID 999 тут).

Рішення: якщо власність невірна — виправте її перед запуском Postgres, а не після того, як він впаде в середині відновлення.

Завдання 11: Мігруйте bind-маунт за допомогою rsync (зберегти власність, xattr, ACL)

cr0x@server:~$ sudo rsync -aHAX --numeric-ids --delete /srv/api/config/ cr0x@newhost:/srv/api/config/
sending incremental file list
./
app.yaml
secrets.env

sent 24,198 bytes  received 91 bytes  48,578.00 bytes/sec
total size is 18,422  speedup is 0.76

Значення: -aHAX зберігає метадані; --numeric-ids уникає невідповідностей імен користувачів між хостами.

Рішення: якщо rsync повідомляє про масивні видалення несподівано — зупиніться і підтвердьте, що ви не вказали неправильний шлях.

Завдання 12: Перевірте контрольні суми для bind-маунтованої директорії (цілісність, а не оптимізм)

cr0x@server:~$ cd /srv/api/config && sudo find . -type f -maxdepth 2 -print0 | sort -z | xargs -0 sha256sum > /tmp/api-config.sha256
cr0x@server:~$ head /tmp/api-config.sha256
b2e58d0c5c2de0c8c61b49e0b8d8c0c95bdb9b5c5b718e3cc7c4f2c1f8f91ac4  ./app.yaml
c1aa9b3e0c6c4e99c7b2e5fd3d9b2a7a1bb6c32d08d5f9d7c7089a8d77c0e0a2  ./secrets.env

Значення: маніфест контрольних сум. Запустіть те саме на призначенні й порівняйте.

Рішення: для критичної конфігурації/стану порівняння контрольних сум краще, ніж «виглядає нормально».

Завдання 13: Виявте невідповідності UID/GID, які зламають контейнери після міграції

cr0x@server:~$ stat -c '%n %u:%g %a' /srv/api/config
/srv/api/config 0:1002 750

cr0x@server:~$ getent group api
api:x:1002:

Значення: директорія належить групі з GID 1002. Якщо на новому хості група api матиме інший GID, груповий доступ зламається.

Рішення: стандартизуйте призначення UID/GID (або використовуйте числові ID послідовно) для bind-маунтованих шляхів.

Завдання 14: Перевірте режим SELinux та контекст (bind-маунти обожнюють тут ламатися)

cr0x@server:~$ getenforce
Enforcing

cr0x@server:~$ ls -Zd /srv/api/config
unconfined_u:object_r:default_t:s0 /srv/api/config

Значення: SELinux увімкнений в режимі Enforcing, а директорія має default_t, що часто блокує доступ контейнера.

Рішення: використовуйте коректні мітки (або опції маунту в описі контейнера). Міграція даних без відновлення контекстів дасть «permission denied» з додатковими кроками.

Завдання 15: Спостерігайте симптоми затримки IO у реальному часі (чи не стало міграція регресією в сховищі?)

cr0x@server:~$ iostat -x 1 3
Linux 6.8.0 (server) 	01/02/2026 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           7.42    0.00    2.13    9.77    0.00   80.68

Device            r/s     w/s   rMB/s   wMB/s avgrq-sz avgqu-sz await r_await w_await  svctm  %util
nvme0n1         21.0   180.0    1.2    14.8    177.4     2.10  11.6    2.1   12.7   0.45  90.4

Значення: await ≈11ms і %util ≈90% вказують, що диск зайнятий і затримки немалі.

Рішення: якщо хост-призначення показує гіршу латентність, ваша міграція «спрацювала», але база даних голосно поскаржиться. Виправте клас сховища перед тим, як звинувачувати Docker.

Завдання 16: Підтвердіть, який тип маунта використовує запущений контейнер (перевірка після переключення)

cr0x@server:~$ docker inspect api01 --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
bind /srv/api/config -> /app/config

Значення: після міграції підтвердіть, що контейнер використовує очікуваний тип маунта і шлях.

Рішення: якщо ви очікували bind-маунт, а бачите volume — ви випадково змінили семантику. Зупиніть і виправте, перш ніж накопичити різнорідний стан.

Швидкий план діагностики

Ви мігрували. Щось повільно, ламається або «працює на одному хості, але не на іншому». Почніть тут. Ця послідовність спрямована швидко знайти вузьке місце,
а не задовольнити цікавість щодо кожного шару.

1) Підтвердіть, що монтується саме те, що ви думаєте

  • Запустіть docker inspect <container> і перевірте .Mounts на Type, Source і Destination.
    Дивна кількість аварій — це «змонтована порожня директорія».
  • Для bind-маунтів запустіть findmnt -T /host/path, щоб переконатися, що він підкріплений потрібною файловою системою/пристроєм.

2) Перевірте права й мітки безпеки

  • Порівняйте вивід stat на вихідному й цільовому хості. Зсув UID/GID поширений після перебудов.
  • Якщо SELinux увімкнений, перевірте контексти за допомогою ls -Z. Якщо працює AppArmor — дивіться обмеження профілю (часто видно в логах контейнера).

3) Перевірте здоров’я сховища перед налаштуванням застосунку

  • Подивіться на латентність пристрою (iostat -x) і насичення файлової системи (df -h, df -i).
  • Якщо це база даних і ви бачите таймаути — вважайте, що винна fsync-латентність, доки не доведено протилежне.

4) Лише потім інспектуйте Docker-специфічні шари

  • Підтвердіть корінь Docker (docker info), драйвер зберігання і чи використовується rootless-режим.
  • Якщо ви копіювали директорію даних Docker між хостами — зупиніться і перевірте сумісність версій движка та драйвера зберігання, перш ніж копати далі.

Парафразована ідея, часто приписувана Werner Vogels: «Все ламається, постійно; проєктуйте й експлуатуйте так, ніби це правда».

Три корпоративні історії (як люди реально отримують проблеми)

Історія 1: Інцидент через неправильне припущення

Середня компанія запускала білінговий сервіс з Postgres у Docker. Compose-файл використовував іменований том: billing_pgdata.
Модель команди була: «іменований том = персистентний, персистентний = переживе заміну хоста.»
Ніхто не записав, де лежать байти. Ніхто не мусив. Поки не довелося.

Рутинний апгрейд ОС перетворився на «ми перебудуємо інстанс». Інфра-команда термінувала старий VM після того, як новий пройшов health checks.
Контейнери швидко піднялися. Postgres теж — на свіжому, порожньому томі, створеному на новому хості. Застосунок теж запустився і почав
щасливо писати нові рядки в нову базу, яка виглядала повністю робочою. Алерти мовчали, бо аптайм був нормальний.

Перший людський сигнал з’явився, коли фінанси запитали, куди поділися вчорашні рахунки. До того часу дані зі старого хоста були втрачені.
«Але це був том» виявився твердженням про API Docker, а не про життєвий цикл інфраструктури.

Виправлення не було героїчним. Вони відновили з бекапів (на щастя, вони були) і написали runbook міграції, що трактує іменовані томи
як експортовані артефакти. Також додали канаркову перевірку: застосунок відмовляється стартувати, якщо не знайдено підпис схеми.
Індент став уроком про припущення. Дані не зникли. Зникло їхнє розуміння.

Історія 2: Оптимізація, що повернулась боком

Інша організація захотіла «чистіші Compose-файли» і менше шляхів хоста. Вони замінили bind-маунти іменованими томами скрізь,
включно з write-heavy сервісом на SQLite (так, у продакшені; таке трапляється).
Розуміння: іменовані томи зменшують помилки шляхів і «Docker краще управляє».

Продуктивність просіла після міграції — не тому, що іменовані томи повільні самі по собі, а через те, де вони опинилися.
Корінь Docker лежав на меншому розділі, підключеному до мережевого сховища, оптимізованого для пропускної здатності, а не для латентності.
Патерн fsync у SQLite карав таке сховище. Застосунок не падав; він просто гіршав у способах, які виглядали як нестача CPU і
«можливо треба більше інстансів», хоча під капотом не було ніяких подів.

Команда ганялася за примарами: налаштування PRAGMA, додавання кешів, зміна ORM. Справжня причина була прозаїчною:
вони перемістили stateful IO з швидкого локального диска (де був bind-маунт) на високолатентний рівень сховища (де жив /var/lib/docker).
Ті самі байти. Інша фізика.

Відкат був так само прозаїчним: повернули базу на bind-маунт до швидкого диска, зберегли іменовані томи для менш критичних даних,
і стали вважати розташування Docker root як першокласне рішення з точки зору ємності/продуктивності. Оптимізація не була помилковою концептуально.
Вона була помилковою в контексті. Контекст — це місце, де живуть інциденти.

Історія 3: Нудна, але правильна практика, що врятувала день

Глобальне підприємство запускало внутрішні сервіси на Docker-хостах, які часто замінювалися автоматичною трубою.
Нічого фантастичного. Нудна частина: кожен хост мав виділену розкладку маунтів для стану:
/srv/state/<service>, з окремою файловою системою для кожного важливого stateful-компонента. Кожна файловa система мала політики снапшотів і бекапу.

Їхні контейнери використовували bind-маунти виключно для стану. Compose-файли не були «портативними» в сенсі, що їх неможливо було просто запустити на локальній машині без створення директорій.
Але продакшн-середовище було консистентним, а консистентність — це, по суті, аптайм у плащі.

Коли хост постраждав від корупції файлової системи (таке трапляє), реакція була майже нудною:
вивести хост з обігу, відновити уражену файлову систему зі снапшота, прикрутити її до замінного хоста, перезапустити контейнери.
Звіт про інцидент був коротким, бо проєкт системи вже передбачав заміну.

Їхня секретна зброя не була інструментом. Це була практика: щоквартально вони репетирували відновлення стану одного сервісу на свіжому хості.
Не тому, що чекали інциденту. А тому, що не довіряли відчуттю, що все «має працювати».

Жарт №2: Єдина річ більш постійна, ніж Docker-контейнер, — це тимчасове рішення, яке ви використали, щоб мігрувати його дані.

Поширені помилки: симптом → корінна причина → виправлення

1) «Запустилося добре, але всі дані зникли»

Симптом: сервіс стартує з порожньою базою або дефолтною конфігурацією після міграції.

Корінна причина: шлях bind-маунта не існував або не був змонтований; Docker створив порожню директорію і змонтував її.
У випадку іменованих томів — створили новий том замість імпорту старих даних.

Виправлення: перевірте маунти хоста за допомогою findmnt; додайте pre-start перевірки (підпис схеми, очікувані файли).
Закріпіть імена томів у Compose; експортуйте/імпортуйте томи явно.

2) «Permission denied» всередині контейнера після переміщення даних

Симптом: логи застосунку показують помилки читання/запису в маунтовані шляхи; Postgres скаржиться на власність директорії даних.

Корінна причина: невідповідність UID/GID (особливо між дистрибутивами), відмінності в user namespaces/rootless-режим, або відсутні ACL/xattr.

Виправлення: мігруйте з rsync -aHAX --numeric-ids; стандартизуйте UID/GID; розгляньте запуск контейнера з явним user:.
Для SELinux — встановіть коректні контексти або використайте відповідні опції маркування маунта.

3) «Працює на Ubuntu, але падає на RHEL»

Симптом: bind-маунтовані шляхи недоступні; контейнери не мають доступу до файлів, які існують.

Корінна причина: SELinux увімкнений і контексти директорії неправильні.

Виправлення: застосуйте коректні SELinux-маркери до директорії або використайте відповідні опції маунту в контейнері й політики.
Перевірте за допомогою ls -Z перед тим, як оголосити Docker «зламаним».

4) «Міграція пройшла, але продуктивність жахлива»

Симптом: збільшена латентність, таймаути, повільні запити, високий IOwait.

Корінна причина: дані тома перемістилися на повільніший клас сховища (часто тому, що Docker root на іншому диску),
або опції файлової системи відрізняються (barriers, режим журналювання, atime).

Виправлення: підтвердіть пристрій і файлову систему через findmnt, iostat -x, df.
Розмістіть stateful bind-маунти на потрібному диску. Якщо використовуєте іменовані томи — перемістіть Docker root на відповідну файлову систему або застосуйте відповідний драйвер томів.

5) «Ми скопіювали /var/lib/docker і тепер Docker не стартує / контейнери пошкоджені»

Симптом: помилки демона Docker, відсутні шари, проблеми з overlay filesystem.

Корінна причина: невідповідність версій движка, драйвера зберігання, несумісність файлової системи або часткова копія без xattr.

Виправлення: уникайте стратегії «мігрувати весь Docker data root», якщо ви не тестували це для точно такого середовища.
Експортуйте/імпортуйте іменовані томи та перебудовуйте образи з реєстрів. Для аварійного відновлення — підлаштуйте версії й копіюйте з метаданими.

6) «Бекапи є, але відновлення не працює»

Симптом: бекапи відновлюють файли, але сервіс не стартує або дані неконсистентні.

Корінна причина: ви бекапили живу директорію бази без погоджених з додатком снапшотів; або відновили без прав/xattr.

Виправлення: використовуйте нативні бекапи бази (або координовані снапшоти). Для бекапів на рівні файлової системи — зупиняйте сервіс або використовуйте можливості снапшотів правильно.

Контрольні списки / покроковий план

План A: Якщо хочете, щоб міграції були нудними (рекомендовано)

  1. Інвентаризація маунтів: запустіть docker ps і docker inspect, щоб перелічити stateful-шляхи й томи.
  2. Класифікуйте дані: база даних, завантаження користувачів, кеш, артефакти збірки, конфіг, секрети. Лише частина цього заслуговує на біль.
  3. Визначте власність сховища:
    • Критичний стан: bind-маунт до /srv/state/<service> на виділеній файловій системі.
    • Відновлюваний стан: іменовані томи прийнятні.
  4. Стандартизуйтесь розкладку хостів: однакові точки маунту на всіх хостах, бажано provision через автомацію.
  5. Стандартизувати ідентичність: забезпечте, щоб системні акаунти й числові ID не дрейфили між хостами.
  6. Резервуйте правильне: файлові системи bind-маунтів через ваш звичайний бекап; іменовані томи через періодичні експорт-завдання (tar у бекап).
  7. Репетируйте відновлення: раз на квартал оберіть сервіс і відновіть його на свіжому хості. Заміряйте час до працездатності.
  8. Додайте запобіжники: застосунок не стартує без очікуваного підпису даних; алерт на «fresh init» події.

План B: Якщо ви вже всюди використали іменовані томи

  1. Перелічіть іменовані томи: docker volume ls і зіставте їх із контейнерами.
  2. Експортуйте томи: tar кожного іменованого тому у шлях резервного копіювання з послідовною схемою імен.
  3. Зберігайте з цілісністю: обчислюйте контрольні суми tar-архівів і зберігайте поряд із ними.
  4. Імпортуйте на призначенні: створіть томи спочатку, потім розпакуйте tar-архіви в них.
  5. Перевірте власність і вміст: перелікуйте ключові директорії всередині тому через тимчасовий контейнер.
  6. Переключення: запустіть сервіси, виконайте smoke-тести, підтвердьте наявність даних (не лише health endpoints).

План C: Якщо вам потрібна «найшвидша можлива» заміна хоста

Тут ви платите за попередні рішення. Швидка заміна можлива, коли стан живе на сховищі, яке можна від’єднати/підключити або реплікувати
незалежно від обчислювального хоста.

  1. Розміщуйте стан на від’єднувальній одиниці: окремий диск, dataset або мережевий том.
  2. Bind-маунтіть цю одиницю: контейнери монтують /srv/state шляхи, що вказують на ту одиницю.
  3. Автоматизуйте прикріплення: замінний хост завантажується, прикріплює сховище, монтує його й потім стартує контейнери.
  4. Тестуйте відмову: симулюйте втрату хоста і вимірюйте час відновлення. Якщо ви цього не вимірюєте — у вас цього немає.

FAQ

1) Чи іменовані томи «безпечніші» за bind-маунти?

Вони безпечніші від певних людських помилок (неправильний шлях, випадковий маунт /), але не обов’язково кращі для довговічності.
Довговічність походить від того, яке сховище під ними і від дисципліни бекапів/відновлення.

2) Чи можна просто копіювати /var/lib/docker/volumes для міграції іменованих томів?

Іноді можна. Достатньо людей це робило, щоб здатися нормальним. Але це залежить від версії движка, драйвера зберігання і деталей файлової системи.
Якщо вам потрібна відтворювана міграція — експортуйте/імпортуйте через tar (або використайте драйвер, що забезпечує портативність).

3) Чому bind-маунти «краще переживають міграції», якщо вони прив’язані до шляхів хоста?

Тому що шлях — це ваш контракт, і ви можете його забезпечити автомацією. Bind-маунт — це просто звичайні дані на звичайній файловій системі.
Це добре працює з дорослими інструментами резервного копіювання/реплікації. Іменовані томи теж звичайні файли, але приховані під управлінням Docker і його розміщенням.

4) А Docker Desktop на macOS/Windows?

Движок працює всередині VM. Іменовані томи живуть в тій VM. Bind-маунти перетинають межу VM і мають свої особливості продуктивності й семантики.
Для міграцій ставте Desktop-томи в категорію «локальні для машини», якщо ви не експортували їх навмисно.

5) Що швидше: bind-маунт чи іменований том?

На Linux з локальним сховищем продуктивність зазвичай схожа, бо обидва приводять до IO файлової системи. Реальні відмінності — це:
де фізично знаходяться дані, опції маунту і шари безпеки. Вимірюйте на вашому залозі; не покладайтеся на фольклор.

6) Чи допомагають іменовані томи з правами доступу?

Вони можуть допомогти, бо Docker створює і володіє директорією під своїм коренем і часто підтримує її консистентною. Але права доступу все одно важливі всередині тому.
Якщо ваш контейнер працює не як root — все одно потрібна коректна власність.

7) Яка найкраща практика для баз даних у Docker?

Розміщуйте базу даних на сховищі, яке можна надійно бекапити й відновлювати. Для багатьох організацій це означає bind-маунт до виділеної файлової системи або керованого блочного пристрою,
плюс інструменти для бекапів, погоджені з додатком. Іменовані томи прийнятні, якщо ваші експорт/імпорт і бекапи однаково дисципліновані.

8) Як запобігти «змонтована порожня директорія» аваріям з bind-маунтами?

Створюйте директорію і монтуйте її через автомацію. Додайте pre-start перевірку: якщо в директорії немає очікуваного маркера, швидко фейліть.
І перевіряйте маунти хоста з findmnt під час деплойменту.

9) Якщо я використовую Docker Compose, чи варто віддавати перевагу іменованим томам для портативності?

Віддавайте перевагу портативності лише якщо вона не шкодить відновлюваності. Для розробки: іменовані томи відмінні. Для продакшн-стану: використовуйте bind-маунти до стабільних шляхів на хості,
або приймайте іменовані томи, але тримайте експорт/імпорт як частину життєвого циклу деплойменту.

10) А драйвери томів (NFS, хмара тощо)?

Вони можуть вирішити міграцію, помістивши дані на спільне або від’єднувальне сховище, але додають нову зону відмов: сам драйвер і мережевий/сервісний шар сховища.
Використовуйте їх, коли потрібен спільний стан або швидке переміщення, і протестуйте їхню поведінку при відмовах під навантаженням.

Практичні наступні кроки

  1. Зробіть аудит сьогодні: перелічіть усі маунти і класифікуйте, які з них — бізнес-стан. Якщо ви не можете назвати — ви не можете його захистити.
  2. Виберіть один шлях міграції і стандартизуйте:
    bind-маунти на консистентну розкладку /srv/state, або іменовані томи з скриптованими експортами і перевіркою контрольних сум.
  3. Перемістіть Docker root свідомо: якщо іменовані томи важливі для вас, файловa система під DockerRootDir — це продукційне сховище.
    Поводьтеся з нею відповідно.
  4. Напишіть runbook «відновити під тиском»: включіть точні команди для експорту/імпорту, виправлення власності, перевірки наявності даних і підтвердження продуктивності.
  5. Репетируйте: зробіть повне відновлення на свіжому хості. Зафіксуйте час. Виправте болючі місця. Повторюйте, поки це не стане нудним.

Bind-маунти не магічні. Іменовані томи не злі. Різниця в тому, чи ваш план міграції залежить від внутрішностей Docker чи від фундаментів файлової системи.
Якщо хочете, щоб міграції були рутинними, будуйте на фундаменті.

← Попередня
Відображення ACL ZFS для SMB: як уникнути кошмарів з правами
Наступна →
MySQL проти PostgreSQL на VPS з 1 ГБ ОЗП: що реально придатне (і які налаштування це забезпечують)

Залишити коментар