Підводний камінь mount propagation в Docker: чому ваш bind‑mount виглядає порожнім (і як виправити)

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

Ви змонтували директорію хоста в контейнер через bind‑mount. Всередині контейнера вона порожня. Ви перевіряєте шлях ще раз. Пробаєте іншу оболонку. Перезапускаєте Docker. Починаєте торгуватися з всесвітом. На хості в директорії купа файлів, але контейнер наполягає, що це ідеальна пустка.

Часто це не проблема прав доступу, не SELinux і не «просто Docker». Це mount propagation — те, як події монтування (нові маунти) передаються або не передаються між таблицями маунтів хоста й namespace контейнера. Це тонко. Це Linux. І це можна виправити.

Точний симптом і чому він вводить в оману розумних людей

Визначимо точно режим відмови «bind‑mount виглядає порожнім», бо «порожній» може означати три різні речі:

  1. Справді порожній: всередині контейнера ви бачите порожню директорію, тоді як на хості там є файли.
  2. Відсутній вміст підмаунта: директорія містить файли, але змонтована під нею файловка (наприклад /mnt/data або /var/lib/kubelet/pods) всередині контейнера виглядає порожньою або як проста папка.
  3. Застарілий вигляд: контейнер бачить старий вміст, але маунти, зроблені після старту контейнера, не з’являються.

Перший випадок зазвичай про права доступу, невідповідність UID/GID або SELinux/AppArmor. Але другий і третій — класичні баги з mount propagation. Ви змонтували щось нижче bind‑mount джерела на хості, а контейнер не отримав цю інформацію.

Ментальна пастка: bind‑mountи відчуваються як «вікно в хост». Насправді це посилання на шлях, який резольвиться в mount namespace із конкретною семантикою propagation. Linux робить саме те, що ви попросили, а не те, що ви мали на увазі.

Короткий жарт №1: Якщо хочете відчути себе всемогутнім, створіть mount namespace; ви зможете зробити так, що / означає все, що забажаєте. Це як подорож у часі, але з більшим числом mount прапорців.

Цікаві факти та історія (коротко, конкретно, корисно)

  • Mount propagation з’явився в основній гілці Linux ще в епосі 2.6 як частина більшого набору функцій для підтримки контейнерів: окремі mount namespace із контрольованим обміном між ними.
  • Функція «shared subtree» існує через рекурсивні маунти. Простого bind‑mountа замало, якщо ви хочете, щоб події маунту під директорією відображалися в іншому місці.
  • Docker спочатку покладався на дефолти Linux. Ці дефолти відрізняються між дистрибутивами й конфігураціями init‑системи, тому одна й та сама команда Docker може поводитися інакше на різних хостах.
  • Systemd змінив правила гри. На багатьох системах під управлінням systemd / (або ключові точки маунту) за замовчуванням ставляться як shared, що впливає на те, як маунти поширюються в сервісах і контейнерах.
  • Kubernetes робить mount propagation явним (mountPropagation: HostToContainer тощо), бо плагіни сховища і CSI‑драйвери постійно потрапляли на ці проблеми.
  • «rshared» — це не «більше прав доступу». Це про події монтування, а не про доступ до файлів. Ви можете мати ідеальні права й все одно бачити порожні підмаунти.
  • OverlayFS зробив кореневі файлові системи контейнерів дешевшими, але не позбавив потреби розуміти маунти; це зробило простіше забути, що ви насуваєте шари файлових систем.
  • Bind‑mountи можуть маскувати маунти. Якщо ви монтуєте поверх існуючої точки маунту, ви приховуєте те, що там було змонтовано. Іноді «порожньо» означає «ви змонтували поверх».
  • Рантайми контейнерів обирають безпечні дефолти. Багато з них за замовчуванням використовують rprivate для bind‑mountів, щоб уникнути ситуацій, коли контейнери впливають на таблицю маунтів хоста.

Mount propagation: що насправді відбувається

У Linux є концепція, яка називається mount namespace. Кожен namespace має свій власний вигляд таблиці маунтів: що де змонтовано й з якими прапорами. Контейнери використовують це, щоб процеси в контейнері могли монтувати без «попису» таблиці маунтів хоста (і навпаки).

Але маунти — це не лише статична таблиця. Це події. Процес може змонтувати файлову систему в /mnt/thing після старту вашого контейнера. Чи з’явиться цей новий маунт всередині контейнера — залежить від propagation між двома namespace.

Shared, private, slave: три ролі

Кожна точка маунту має тип propagation. На практиці ви зустрінете:

  • private: події маунту/відмаунту під цією точкою не поширюються на інших.
  • shared: події маунту/відмаунту поширюються на всі маунти в тій самій групі peer.
  • slave: приймає поширення від master shared маунта, але не відправляє події назад.

Є також рекурсивні варіанти:

  • rshared, rprivate, rslave: правило застосовується до маунта й усіх підмаунтів під ним.

Ось підступ: bind‑mountи Docker часто створюються з propagation, яке не є рекурсивно shared. Тож якщо хост змонтує щось під шляхом джерела пізніше, контейнер цього не побачить.

Типова реальна ситуація, що викликає баг

Ви робите щось на кшталт:

  • Bind‑монтуєте /srv в контейнер як /srv.
  • На хості пізніше монтуєте NFS у /srv/data (наприклад systemd змонтував на вимогу).
  • Всередині контейнера /srv/data виглядає порожнім або як звичайна директорія, бо підмаунт не передався.

Це не проблема видимості файлів. Це проблема видимості підмаунта.

Одит цитата — бо опси колекціонують такі як шрами

Everything fails all the time. — Werner Vogels

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

Якщо bind‑mount виглядає порожнім, ви можете витратити цілий день — або пройти цей чекліст за п’ять хвилин і знати, де закопано тіло.

  1. Підтвердіть, який саме «порожній» випадок. Чи файли відсутні, чи лише вміст підмаунта?
  2. Перевірте, чи хост‑шлях містить точку маунту під ним. Якщо так — підозрюйте propagation негайно.
  3. Перегляньте налаштування propagation для bind‑маунта в контейнері. Шукайте Propagation у виводі docker inspect.
  4. Порівняйте вигляд таблиці маунтів у namespace. Використайте nsenter, щоб подивитися, що бачить контейнер.
  5. Рішення: змінити propagation, змінити архітектуру або перестати монтувати під цим шляхом. Є «правильне» виправлення й «термінове» — оберіть усвідомлено.

Скорочення: якщо ви динамічно монтуєте щось під директорією й одночасно bind‑монтуєте цю директорію в контейнери, вам майже завжди потрібен rshared або rslave. За замовчуванням rprivate — безпечно, але дивно.

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

Завдання 1: Відтворіть баг у контрольованому середовищі

cr0x@server:~$ sudo mkdir -p /srv/demo/submount
cr0x@server:~$ echo "host-file" | sudo tee /srv/demo/host.txt
host-file
cr0x@server:~$ docker run --rm -d --name mp-test -v /srv/demo:/demo alpine sleep 100000
b8a3b2c55d9f8e8a2c5a2b0f0d2f0b6d9a5f9d0c8b7a0d1c2b3a4f5e6d7c8b9
cr0x@server:~$ docker exec mp-test ls -la /demo
total 8
drwxr-xr-x    3 root     root          4096 Jan  3 12:00 .
drwxr-xr-x    1 root     root          4096 Jan  3 12:00 ..
-rw-r--r--    1 root     root             10 Jan  3 12:00 host.txt
drwxr-xr-x    2 root     root          4096 Jan  3 12:00 submount

Що це означає: базовий bind‑маунт працює. Контейнер бачить існуючі файли.

Рішення: продовжуйте й змонтуйте щось під /srv/demo/submount на хості, щоб перевірити, чи контейнер успадкує це.

Завдання 2: Створіть маунт під bind‑маунтом (на стороні хоста)

cr0x@server:~$ sudo mount -t tmpfs tmpfs /srv/demo/submount
cr0x@server:~$ echo "mounted-file" | sudo tee /srv/demo/submount/mounted.txt
mounted-file
cr0x@server:~$ mount | grep "/srv/demo/submount"
tmpfs on /srv/demo/submount type tmpfs (rw,relatime)

Що це означає: тепер під директорією, яку ви зв’язали в контейнер, є підмаунт.

Рішення: перевірте, чи контейнер бачить mounted.txt. Якщо ні — це propagation.

Завдання 3: Перевірте видимість всередині контейнера

cr0x@server:~$ docker exec mp-test sh -lc "mount | grep /demo/submount || true; ls -la /demo/submount; cat /demo/submount/mounted.txt 2>/dev/null || echo 'no mounted.txt'"
total 8
drwxr-xr-x    2 root     root          4096 Jan  3 12:00 .
drwxr-xr-x    3 root     root          4096 Jan  3 12:00 ..
no mounted.txt

Що це означає: контейнер не бачить tmpfs‑підмаунт; він бачить стару директорію натомість.

Рішення: підтвердьте режим propagation і виправте на rshared/rslave залежно від потреб безпеки.

Завдання 4: Перегляньте налаштування propagation bind‑маунта

cr0x@server:~$ docker inspect mp-test --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/demo","Destination":"/demo","Mode":"","RW":true,"Propagation":"rprivate"}]

Що це означає: propagation — rprivate. Нові маунти під /srv/demo не покажуться в контейнері.

Рішення: переключіть на rshared або rslave для цього маунта.

Завдання 5: Перезапустіть контейнер з propagation rshared

cr0x@server:~$ docker rm -f mp-test
mp-test
cr0x@server:~$ docker run --rm -d --name mp-test -v /srv/demo:/demo:rshared alpine sleep 100000
a0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
cr0x@server:~$ docker inspect mp-test --format '{{range .Mounts}}{{.Destination}} {{.Propagation}}{{"\n"}}{{end}}'
/demo rshared

Що це означає: bind‑маунт тепер налаштований на поширення підмаунтів.

Рішення: перевірте, чи існуючий tmpfs‑підмаунт став видимий.

Завдання 6: Підтвердіть видимість підмаунта після зміни propagation

cr0x@server:~$ docker exec mp-test sh -lc "mount | grep /demo/submount; ls -la /demo/submount; cat /demo/submount/mounted.txt"
tmpfs on /demo/submount type tmpfs (rw,relatime)
total 4
drwxr-xr-x    2 root     root            60 Jan  3 12:01 .
drwxr-xr-x    3 root     root          4096 Jan  3 12:01 ..
-rw-r--r--    1 root     root            13 Jan  3 12:01 mounted.txt
mounted-file

Що це означає: контейнер тепер бачить підмаунт хоста. Виправлення propagation підтверджено.

Рішення: вирішіть, чи підходить вам rshared, або оберіть rslave для односпрямованого поширення.

Завдання 7: Оберіть rslave, якщо потрібне лише односпрямоване поширення

cr0x@server:~$ docker rm -f mp-test
mp-test
cr0x@server:~$ docker run --rm -d --name mp-test -v /srv/demo:/demo:rslave alpine sleep 100000
d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9
cr0x@server:~$ docker exec mp-test sh -lc "mount | grep /demo/submount; cat /demo/submount/mounted.txt"
tmpfs on /demo/submount type tmpfs (rw,relatime)
mounted-file

Що це означає: rslave дозволяє host → container події маунту, але не пропускатиме маунти контейнера назад на хост.

Рішення: у більшості продакшн‑випадків віддавайте перевагу rslave, якщо вам не потрібне двостороннє поширення.

Завдання 8: Підтвердіть стан propagation на хості (shared vs private)

cr0x@server:~$ findmnt -o TARGET,PROPAGATION /srv/demo
TARGET   PROPAGATION
/srv/demo shared

Що це означає: точка маунту на хості може бути в shared peer‑групі. Якщо вона private, вам можливо потрібно змінити й сторону хоста.

Рішення: якщо propagation на хості — private і вам потрібне sharing, можливо, доведеться застосувати mount --make-rshared (обережно).

Завдання 9: Безпечно подивіться різницю між namespace за допомогою nsenter

cr0x@server:~$ pid=$(docker inspect -f '{{.State.Pid}}' mp-test)
cr0x@server:~$ sudo nsenter -t "$pid" -m -- findmnt -R /demo | head
TARGET            SOURCE         FSTYPE OPTIONS
/demo             /dev/sda1      ext4   rw,relatime
/demo/submount    tmpfs          tmpfs  rw,relatime

Що це означає: ви дивитесь на маунти всередині mount namespace контейнера без залежності від інструментів контейнера.

Рішення: якщо в поданні namespace немає підмаунта — це propagation або timing. Якщо є — перевіряйте шлях застосунку.

Завдання 10: Виявіть помилки «змонтували поверх» (masking)

cr0x@server:~$ sudo mount -t tmpfs tmpfs /srv/demo
cr0x@server:~$ ls -la /srv/demo | head
total 8
drwxr-xr-x  2 root root   40 Jan  3 12:02 .
drwxr-xr-x  3 root root   18 Jan  3 12:00 ..

Що це означає: ви змонтували tmpfs поверх /srv/demo, приховавши оригінальний вміст. Вітаємо, ви створили «порожнє» чесним шляхом.

Рішення: відмонтуйте і перепроєктуйте: не монтуйте поверх джерела bind, якщо не хочете його замінити.

Завдання 11: Перегляньте маунти з врахуванням рекурсії

cr0x@server:~$ sudo umount /srv/demo
cr0x@server:~$ findmnt -R /srv/demo
TARGET            SOURCE  FSTYPE OPTIONS
/srv/demo         /dev/sda1 ext4  rw,relatime
/srv/demo/submount tmpfs   tmpfs  rw,relatime

Що це означає: -R показує підмаунти. Якщо ви просто запустите mount | grep /srv/demo, ви можете пропустити вкладений маунт.

Рішення: якщо «відсутні дані» живуть на підмаунті — ставтеся до цього як до проблеми propagation, поки не доведете протилежне.

Завдання 12: Перевірте проблеми з SELinux (щоб уникнути хибних висновків)

cr0x@server:~$ docker run --rm -v /srv/demo:/demo:Z alpine sh -lc "ls -la /demo | head"
total 8
drwxr-xr-x    3 root     root          4096 Jan  3 12:00 .
drwxr-xr-x    1 root     root          4096 Jan  3 12:03 ..
-rw-r--r--    1 root     root             10 Jan  3 12:00 host.txt

Що це означає: на системах із виконанням SELinux опція :Z перемітить контент для доступу контейнера. Якщо це виправляє «порожньо» — то це була не propagation.

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

Завдання 13: Перевірте профіль AppArmor (рідко, але може дошкуляти)

cr0x@server:~$ docker inspect mp-test --format '{{.AppArmorProfile}}'
docker-default

Що це означає: контейнер працює під профілем (звично на Ubuntu). Зазвичай це впливає на спроби маунтів зсередини контейнера, а не на видимість маунтів хоста.

Рішення: якщо контейнер має виконувати маунти (наприклад FUSE, поведінка на кшталт CSI), можливо, потрібні додаткові привілеї або інша архітектура.

Завдання 14: Перевірте, чи сам шлях‑джерело — точка маунту з propagation «private»

cr0x@server:~$ findmnt -o TARGET,PROPAGATION /
TARGET PROPAGATION
/      shared

Що це означає: якщо / або релевантний батьківський маунт має propagation private, ви можете отримати дивну поведінку з вкладеними маунтами. Дефолти дистрибутивів мають значення.

Рішення: не запускайте автоматично mount --make-rshared / у продакшні. Робіть це лише якщо розумієте потенційні наслідки і краще в час вікна технічного обслуговування.

Завдання 15: Побачте точно, що Docker думає, що він змонтував

cr0x@server:~$ docker inspect mp-test --format '{{range .Mounts}}{{println .Source "->" .Destination "prop:" .Propagation}}{{end}}'
/srv/demo -> /demo prop: rslave

Що це означає: погляд Docker часто достатній, щоб помітити проблему: rprivate, коли ви очікуєте поведінки shared.

Рішення: якщо Docker показує rprivate, не сперечайтеся з ним. Змініть опцію маунту або переробіть структуру.

Рішення, які працюють (і коли їх застосовувати)

Виправлення 1: Встановіть propagation явно на bind‑маунті

Для Docker CLI можна вказати це в специфікації тому:

  • Двостороннє: -v /path:/path:rshared
  • Тільки від хоста до контейнера (зазвичай безпечніше): -v /path:/path:rslave

Коли використовувати: коли на хості під джерелом bind монтують або відмонтовують речі після старту контейнера (NFS automounts, systemd mounts, CSI staging, loop‑маунти, tmpfs).

Коли не використовувати: коли ви не контролюєте, що може робити контейнер, і боїтесь, що події маунту можуть потрапити назад (уникати rshared, якщо воно не потрібно).

Виправлення 2: Припиніть монтувати під директорією, що bind‑монтується

Це нудне архітектурне виправлення: не створюйте підмаунти під шляхом, який ви bind‑монтуєте в контейнери. Монтуйте динамічну файлову систему в інше місце, потім bind‑монтуйте фінальну точку безпосередньо в контейнер.

Приклад: замість bind‑маунту /srv і подальшого монтування NFS у /srv/data, монтуйте NFS у /mnt/nfs/data і bind‑монтуйте /mnt/nfs/data прямо.

Чому це працює: ви взагалі уникаєте залежності від propagation. Менше магії. Менше несподіванок.

Виправлення 3: Зробіть маунт на хості shared (тільки якщо потрібно)

Якщо точка маунту на хості — private, навіть bind‑маунт контейнера з rshared може не давати бажаного результату. У деяких налаштуваннях потрібно, щоб дерево маунтів на хості було shared.

cr0x@server:~$ sudo mount --make-rshared /srv
cr0x@server:~$ findmnt -o TARGET,PROPAGATION /srv
TARGET PROPAGATION
/srv   shared

Що це означає: події маунту під /srv можуть поширюватися на peer‑маунти.

Рішення: робіть це лише для піддерева, яким ви керуєте. Робити / рекурсивно shared може мати несподівані взаємодії з іншими сервісами.

Виправлення 4: Віддавайте перевагу синтаксису --mount для ясності

Синтаксис -v компактний, але легко помилитись. Форма --mount більш розгорнута і менш неоднозначна:

cr0x@server:~$ docker run --rm -d --name mp-test \
  --mount type=bind,source=/srv/demo,target=/demo,bind-propagation=rslave \
  alpine sleep 100000
2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1

Чому це важливо: продакшн‑аварії люблять конфігурації, що «виглядали правильно». Розгорнута конфігурація рідше «виглядає правильно помилково», і це перемога.

Виправлення 5: Якщо контейнеру потрібно виконувати маунти — перегляньте дизайн

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

Суб’єктивна порада: якщо вам потрібно, щоб маунти, ініційовані контейнером, з’являлися на хості, ви будуєте компонент зберігання. Ставтеся до цього як до такого: мінімальні привілеї, суворі обмеження і явна політика propagation. Якщо це не потрібно — не вмикайте «про запас».

Короткий жарт №2: «Просто запустіть у privileged» — це опс‑еквівалент «вимкнути й увімкнути знову», лише що це відкриває нові способи ламати систему.

Три міні‑історії з корпоративного світу

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

Команда перенесла стару пакетну систему в контейнери. Контракт був простий: скидати файли в /incoming, обробляти їх, записувати результати в /outgoing. На віртуалках це були просто директорії. В епосі контейнерів вони bind‑монтували /srv/pipeline в контейнер як /pipeline і зберегли внутрішні шляхи незмінними.

Через місяці зростання використання диска. Хтось додав automount‑unit, щоб /srv/pipeline/incoming під час робочих годин був підключений NFS. Гарна ідея: менше локального диску, легке масштабування. Зміна вийшла в п’ятницю, бо «лише сховище».

Того вікенду конвеєрні воркери почали повідомляти «немає вхідних файлів». Метрики дивували: NFS був повний, контролер пакету правильно його наповнював, воркери виглядали здоровими. On‑call перезапускав воркерів. Нічого не змінювалося.

Корінь проблеми — mount propagation. Воркери стартували раніше, ніж automount спрацював, тому в їхніх namespace /pipeline/incoming лишався простою папкою назавжди. NFS змонтувався на хості, але їхній bind‑маунт був rprivate. Вони ніколи не побачили NFS‑підмаунт.

Виправлення було простим і миттєвим: змінити bind‑маунт на rslave і задеплоїти. Довготривале виправлення було важливішим: припинити bind‑монтувати широкий батько /srv/pipeline; монтувати NFS у стабільний шлях і bind‑монтувати саме ту директорію. Це зменшило радіус ураження при змінах сховища.

Міні‑історія 2: Оптимізація, що дала зворотний ефект

Платформена команда хотіла пришвидшити bootstrap нод. Вони перемістили великі набори даних з локального диска на маунти, що підключаються на вимогу: частина була loop‑маунтами образів, частина — мережевими шарами. Ідея — «активувати» маунти лише коли нода запускає навантаження, яке їх потребує.

Вони також стандартизували аргументи запуску контейнера: кожен сервіс отримав bind‑маунт /opt/runtime, щоб загальні інструменти і сертифікати були доступні. У девелопі й в staging це працювало. У проді почалися збої, що нагадували корупцію даних: додатки бачили порожні директорії там, де мали бути набори даних.

Виявилось, що скрипт «активації» монтував набори даних під /opt/runtime/datasets після того, як контейнери вже були запущені. Bind‑маунт був rprivate, тому контейнери ніколи не отримували нові підмаунти. Інженери намагались «виправити» проблему перезапуском тільки впалих аплікацій, що іноді спрацьовувало — бо випадково маунт був активний до перезапуску. Називали це flaky. Насправді це було детерміновано, лиш невлучний таймінг.

Вони змінили маунт на rshared глобально, бо це швидко усунуло проблему. Через два тижні інший інцидент: дебаг‑контейнер з додатковими привілеями змонтував tmpfs у місці, що поширилось назад на хост‑піддерево. Це не знищило дані, але збило моніторинг і ускладнило cleanup‑скрипти.

Фінальний стан був тонким: rslave для більшості сервісів, суворі набори capability і правило, що лише спеціальний агент може робити маунти. «Оптимізація» не була помилкою; помилка була у непроаналізованих припущеннях щодо propagation і привілеїв.

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

Інша організація мала уніфікований флот, де зберігання було складним: локальні SSD‑кеші, мережеві маунти й іноді перевімонтовання під час інцидентів. У них було просте правило: не bind‑монтувати широкі батьківські директорії. Монтуйте лише те, що контейнер дійсно потребує. Якщо контейнеру потрібно /data/app, він отримує /data/app, а не /data.

Це було непопулярно. Розробникам зручно. SRE теж любили зручність — аж поки їх не підвели. Команда платформи примусово застосовувала правило на рев’ю коду і в CI‑перевірках для Compose‑специфікацій і маніфестів деплою.

Якось планове обслуговування сховища вимагало перевімонтовання піддерева в /data. На хостах, де сервіси історично bind‑монтували /data, це спричинило б часткові баги видимості. Але тут більшість сервісів мали прямі маунти своїх точок, і лише кілька спеціалізованих агентів використовували rslave, бо очікували підмаунтів.

Інцидент завершився нудно. Декілька контейнерів перезапустили в рамках технічного обслуговування, але не було загадкової поведінки «порожньої директорії». Постмортем був короткий і нецікавий — а це найвища похвала для практики операцій.

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

1) «Директорія порожня в контейнері, але не на хості»

  • Симптом: ls показує порожню директорію; помилок немає.
  • Корінна причина: ви змонтували поверх джерельного шляху на хості (masking), або дивитесь на підмаунт, який не поширився.
  • Виправлення: запустіть findmnt -R /host/path, щоб побачити підмаунти; уникайте маунтів поверх джерела bind; встановіть bind-propagation=rslave, якщо підмаунти мають з’являтися.

2) «Файли з’являються після перезапуску контейнера»

  • Симптом: перезапуск контейнера «виправляє» проблему, але не завжди.
  • Корінна причина: залежність від таймінгу: маунт на хості існує під час старту контейнера іноді, іноді ні (automount, on‑demand скрипти).
  • Виправлення: використовуйте rslave/rshared, або забезпечте підняття маунтів до старту контейнерів (через systemd ordering).

3) «Порожні лише піддиректорії»

  • Симптом: базові файли видимі, але окрема вкладена директорія пуста.
  • Корінна причина: та вкладена директорія насправді є точкою маунту на хості.
  • Виправлення: bind‑монтуйте вкладений маунт безпосередньо, або увімкніть propagation для батьківського bind‑маунта.

4) «Працює на Ubuntu, але не на іншому дистрибутиві (або навпаки)»

  • Симптом: ідентична команда контейнера дає різні результати на різних хостах.
  • Корінна причина: різні дефолтні налаштування propagation для / або релевантних точок маунту; systemd‑дефолти відрізняються; дефолти рантайму контейнера відрізняються.
  • Виправлення: не покладайтеся на дефолти; оголошуйте propagation явно; перевіряйте findmnt -o TARGET,PROPAGATION.

5) «Ми встановили rshared і тепер дивні маунти з’являються на хості»

  • Симптом: хост бачить маунти, створені контейнерами, або маунти лишаються після виходу контейнера.
  • Корінна причина: rshared двосторонній; контейнер має можливість монтувати (або запущений privileged) і події поширюються назад.
  • Виправлення: використовуйте rslave для односпрямованого поширення; зменшіть можливості контейнера; уникайте privileged; забезпечте cleanup маунтів через хост‑агент.

6) «Маунт є, але доступ заборонено»

  • Симптом: файли існують, але ви бачите помилки доступу; іноді додатки бачать це як «порожньо».
  • Корінна причина: невідповідність SELinux контекстів, обмеження AppArmor, невідповідність UID/GID, обмеження rootless Docker.
  • Виправлення: на SELinux‑системах використовуйте :Z або коректні лейбли; вирівняйте UID/GID; не плутайте відмови політик з propagation.

7) «Ми bind‑монтували symlink і отримали неправильний шлях»

  • Симптом: контейнер бачить іншу директорію, ніж очікували.
  • Корінна причина: bind‑маунт резольвить symlink під час маунту; пізніші зміни не вплинуть; або symlink веде в маунт, який не поширюється.
  • Виправлення: bind‑монтуйте реальні шляхи; уникайте symlink‑опосередкування для точок зберігання; перевіряйте через readlink -f на хості.

Чеклісти / покроковий план

Покроковий план: виправити продакшн‑інцидент «порожній bind‑mount» без гадань

  1. Визначте точно, що відсутнє. Це файли чи вміст підмаунта? Якщо підмаунт — швидко переходьте до діагностики propagation.
  2. На хості промапте дерево маунтів. Використайте findmnt -R на джерелі bind. Якщо бачите вкладений маунт — знайшли кандидата на причину.
  3. Підтвердіть propagation контейнера. docker inspect і подивіться на поле Propagation. Якщо там rprivate, не чекайте підмаунтів.
  4. Визначте безпечний напрямок поширення.
    • Потрібно лише host → container: обирайте rslave.
    • Потрібно обом напрямкам: обирайте rshared і документуйте рішення.
  5. Задайте явно через --mount. Впровадьте, перезапустіть залежні контейнери.
  6. Перевірте через nsenter або mount всередині контейнера. Не довіряйте лише логам додатку.
  7. Потім виправіть архітектуру. Віддавайте перевагу прямим маунтам тих директорій, які потрібні, а не широким батьківським bind‑маунтам.

Чекліст: коли варто використовувати rslave/rshared

  • Ви використовуєте systemd automount під директорією, яка bind‑монтується в контейнери.
  • Ви монтуєте мережеве сховище (NFS, CIFS) або блочні пристрої під директорією, яку вже використовують контейнери.
  • У вас є sidecar або хост‑агент, що монтує набори даних пізніше (після старту контейнерів).
  • Ви робите щось CSI‑подібне, плагіноподібне або будуєте інтеграцію зі сховищем.

Чекліст: коли уникати rshared

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

FAQ

1) Чому мій bind‑mount показує файли, але змонтовані файлові системи під ним відсутні?

Бо базовий bind‑маунт присутній, але події підмаунту не поширилися в mount namespace контейнера. Встановіть bind-propagation=rslave або rshared, або bind‑монтуйте підмаунт напряму.

2) У чому практична відмінність між rshared і rslave?

rshared поширює події маунту/відмаунту в обидва боки між peer‑маунтами. rslave дозволяє host → container поширення, але не container → host. Для більшості продакшн‑сервісів rslave — більш розумний вибір, якщо потрібне поширення.

3) Чи може mount propagation зробити директорію повністю порожньою?

Так, якщо «реальні дані» знаходяться на вкладеному маунті (NFS, tmpfs, loop device), а контейнер бачить лише підлягаючу директорію. Виглядатиме порожньою або застарілою, бо ви дивитесь не на той шар.

4) Це баг Docker?

Ні. Це поведінка mount namespace Linux разом із (розумними) дефолтами Docker. Баг — це припущення, що bind‑маунт автоматично рекурсивний і динамічно оновлюється підмаунтами.

5) Чи зачіпає це Docker volumes (іменовані томи)?

Може, але найпоширеніше це з bind‑маунтами, бо ви мапите довільний хост‑шлях, де можуть з’являтися підмаунти. Іменовані томи зазвичай керуються в просторі Docker і рідше мають несподівані підмаунти — якщо ви їх не створювали.

6) Як systemd automount взаємодіє з контейнерами?

Automount означає, що маунт відбувається при першому доступі, можливо після старту контейнера. Якщо у контейнера view не отримує подію маунту через rprivate, він може ніколи не побачити змонтовану файлову систему. Використайте rslave і по можливості забезпечте порядок підняття маунтів перед стартом контейнера.

7) А rootless Docker?

Rootless середовища додають обмеження: ви можете не мати можливості змінювати певні propagation або виконувати маунти взагалі. Якщо ви покладаєтеся на динамічні хост‑маунти, що повинні з’являтися всередині rootless контейнера, розгляньте реархітектуру, яка уникає такої залежності.

8) Kubernetes це вирішує автоматично?

Ні. Kubernetes робить це явним. Под може запитувати режими propagation, і кластер має це дозволяти. Якщо ви запускаєте плагіни зберігання або потребуєте, щоб хост‑маунти з’являлися в контейнерах, ви все ще зіткнетеся з тим же базовим семантиками Linux.

9) Який найбезпечніший довгостроковий підхід, щоб уникнути цієї класи проблем?

Не bind‑монтуйте широкі батьківські директорії як /srv або /data. Монтуйте сховище в стабільні, присвячені точки і bind‑монтуйте лише ті точні директорії, які потребує ваш контейнер.

Висновок: наступні кроки, які можна застосувати сьогодні

Якщо запам’ятати одне: bind‑маунт — це не «живий стрім подій маунту», якщо ви явно цього не налаштували. Коли ваш контейнер бачить порожню директорію, а на хості є дані, спочатку запитайте: «Чи ці дані фактично на підмаунті?»

Практичні кроки:

  1. На ураженому хості запустіть findmnt -R на джерелі bind і знайдіть вкладені маунти.
  2. Запустіть docker inspect і перевірте поле Propagation у маунті.
  3. Якщо потрібна видимість host → container підмаунтів, перепроєктуйте з bind-propagation=rslave (або rshared, лише якщо є обґрунтування).
  4. Потім рефакторіть: bind‑монтуйте точні точки, які потрібні вашому додатку, а не увесь батьківський каталог.

Ось різниця між одноразовим виправленням і тим, щоб ніколи більше не відкривати цю сторінку о 03:00.

← Попередня
Docker «manifest unknown»: теги проти дайджестів — пояснення й виправлення
Наступна →
Консоль Proxmox не відкривається (SPICE/noVNC): де ламається і як виправити

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