Files
obsidian-vault/personal/tech/macos-docker-virtiofs-mount-limits.md

15 KiB
Executable File
Raw Permalink Blame History

macOS Docker Desktop — что НЕ проезжает в контейнер (virtiofs)

Создан 2026-09-26. Статус: проверено живыми тестами (alpine:3.20, linux/arm64, Colima на macOS 26.6.1, Apple Silicon; colima start --cpu 8 --memory 24 --disk 100). Тема: границы проброса путей из мира macOS в контейнер. Все выводы получены прогонами, не по памяти. Команды воспроизведения — в §6.


0. Главный вывод одной строкой

На macOS рантайм контейнеров (Colima, Docker Desktop) отдаёт контейнеру файлы не через Linux mount, а через virtiofs — демон, который строит маппинг путей в момент старта контейнера и знает только пути мира macOS, известные ему на старте.

Всё, что появляется позже и внутри VM (ручной mount, шара, новый каталог), изнутри VM видно корректно — до контейнера не доходит. Это не кэш: рестарт не помогает, sleep не помогает.

📌 На Linux-хосте (нативный Docker) этих ограничений нет — там nsenter-трюки с mount --bind работают. Все выводы ниже — про macOS.


1. Симлинки в контейнер НЕ проезжают

Симлинк, чья цель лежит вне смонтированного каталога, в контейнере битый.

host:  /tmp/symtest/outside/file.txt = "REAL CONTENT ON HOST"
bind:  -v /tmp/symtest/bind:/mnt      (внутри /mnt/target -> /tmp/symtest/outside)
контейнер:  cat /mnt/target/file.txt
→ can't open '/mnt/target/file.txt': No such file or directory

Сам симлинк внутри виден (lrwxrwxrwx target -> /tmp/symtest/outside), но цель резолвится относительно корня контейнера, где этого пути нет. Docker монтирует только реальные пути — симлинки не разворачивает.

Следствие: папка с симлинками на внешние цели в контейнере нерабочая, даже если на хосте всё открывается.


2. Live mount --bind (nsenter) контейнеру НЕ виден

Схема обычная для Linux-хоста:

docker run --rm --privileged --pid=host alpine nsenter -t 1 -m \
  mount --bind /host/source /host/agents/target

⚠️ Первая ловушка (до самого mount): nsenter -t 1 -m заходит в VM, а не в macOS. Пути мира macOS там не существуют. Поэтому хост-путь для VM нужно создавать внутри VM — например, в том же nsenter -t 1 -m sh -c '...'.

Проверено тремя прогонами, результат одинаковый:

Прогон Условие Результат
1 каталог создан после старта контейнера + live bind в контейнере пусто
2 ожидание 5 с (гипотеза «кэш атрибутов virtiofs») пусто
3 каталог существовал до старта контейнера + bind поверх него пусто

Изнутри VM всё корректно:

VM:          cat /tmp/bindtest2/agents/existing/f.txt → "DATA2"   ✓ файл есть
mount:       BIND_OK                                              ✓ смонтировалось
контейнер:   cat /agents/existing/f.txt → No such file or directory ✗

Вывод: live-bind для добавления каталогов в контейнер на macOS недоступен. Единственный рабочий путь — -v в compose + пересоздание контейнера (docker compose up -d = 2-3 сек, --privileged не нужен).


3. Сетевые шары (SMB/NFS) в контейнер НЕ протянуть тем же путём

  • mount_smbfs/mount_nfs в macOS создаёт точку в мире macOS → virtiofs её не видит → -v /Volumes/share:/data не работает.
  • mount внутри VM через nsenter технически проходит, но:
    • не переживает рестарт Docker (нужен startup-хук);
    • требует --privileged;
    • SMB/NFS не дают hardlink между экспортами, блокировки файлов ведут себя иначе — сборщики/редакторы на шаре работают хуже, чем на локальном диске.

Рекомендация: не тянуть шару в контейнер. Если данные на NAS — rsync в локальную папку по требованию. Копия переживает всё и не требует privileged.


Это свойство ФС, не Docker:

  1. Hardlink на каталог не делается. Ни в APFS, ни в ext4, ни в NTFS. ln dir1 dir2 → Operation not permitted. У каталога появилась бы ссылка на себя → цикл в дереве. Запрещено на уровне syscall.
  2. Между томами не работает. Только внутри одной ФС (один st_dev), иначе EXDEV: Cross-device link.

Следствие: «подключить проект-каталог hardlink'ом извне» — физически невозможно. Hardlink годится только для файлов внутри одного тома.


5. Матрица: чем подключать путь в контейнер

Способ Папку? Через тома? Видно в контейнере (macOS) Итог
symlink да да ✗ битый ❌ для внешних целей
hardlink ✗ нет ✗ нет — ✓ только файлы внутри тома
live mount --bind (nsenter) да да ✗ ❌ на macOS
-v в compose + recreate да да ✓ ✅ рабочий путь
rsync-копия да да ✓ ✓ когда данные на шаре/NAS

6. Команды воспроизведения

# 1. Симлинк в контейнер
mkdir -p /tmp/symtest/outside /tmp/symtest/bind
echo "REAL CONTENT ON HOST" > /tmp/symtest/outside/file.txt
ln -sfn /tmp/symtest/outside /tmp/symtest/bind/target
docker run --rm --platform linux/arm64 -v /tmp/symtest/bind:/mnt alpine:3.20 \
  sh -c 'ls -la /mnt/; cat /mnt/target/file.txt'      # → No such file or directory

# 2. Live bind (внимание: пути создавать ВНУТРИ VM)
docker run -d --name t -v /tmp/bindtest2/agents:/agents alpine:3.20 sleep 300
docker run --rm --privileged --pid=host alpine:3.20 nsenter -t 1 -m sh -c \
  'mkdir -p /tmp/bindtest2/agents/existing /tmp/bindtest2/ext2
   echo DATA2 > /tmp/bindtest2/ext2/f.txt
   mount --bind /tmp/bindtest2/ext2 /tmp/bindtest2/agents/existing'
docker exec t cat /agents/existing/f.txt              # → No such file or directory

# 3. Hardlink на каталог (должен упасть)
ln /tmp/a /tmp/b                                     # → Operation not permitted

⚠️ Тестовые docker run тянут образы — гонять их на Mac, не на слабом хосте (Kraken: buildkit-кэш и слои лягут на диск зря).


7. Что делать в реальном проекте

  1. Внешние каталоги — только через -v в compose + docker compose up -d. Манифест монтажей (список name + host_path) генерирует compose.override.yaml — composefile не правится скриптом вручную.
  2. auth/секреты CLI — на хосте, ВНЕ рабочего каталога агента. Иначе агент, которому рабочий каталог смонтирован на запись, получает доступ к своим же токенам. Монтировать отдельным -v.
  3. Шары — rsync в локальную папку, не прямой mount.
  4. Hardlink — только файлы внутри одного тома, с проверкой st_dev.
  5. Не строить архитектуру на live-mount'ах — на macOS это тупик.

8. ⚠️ Реализация на macOS: docker-compose, а не docker compose

Проверено 2026-09-26 на этой машине (Docker CLI 29.4.1, macOS 26.6.1, Apple Silicon). Compose здесь — отдельный бинарь, не подкоманда docker:

Что Значение
CLI /opt/homebrew/bin/docker (v29.4.1)
buildx плагин, симлинк в ~/.docker/cli-plugins/docker-buildx
compose /opt/homebrew/bin/docker-compose (v5.1.3), симлинк в /opt/homebrew/lib/docker/cli-plugins/
docker compose config          # → docker: unknown command: docker compose
docker compose config --quiet  # → unknown flag: --quiet
docker-compose config          # ✅ работает
docker-compose config --services

📌 Всегда docker-compose (с дефисом) в этом окружении. Сообщение об ошибке выглядит как «docker не знает такой команды», а не «compose не установлен» — легко принять за сломанный Docker.

⚠️ Дефолтный билдер (default → /var/run/docker.sock) в статусе error. Для arm64-сборок использовать именованный билдер:

docker buildx build --builder arm64builder --platform linux/arm64 \
  --provenance=false --load -t <tag> .

Порядок файлов при генерации override

compose.override.yaml подхватывается compose автоматически, но если он ссылается на несуществующий путь на хосте, up падает на монтировании. Поэтому порядок строго такой:

1. .env                       ← env_file, иначе up падает сразу
2. agents-mounts.yaml         ← манифест (можно пустой: mounts: [])
3. tools/render-mounts.py     ← → compose.override.yaml
4. docker-compose up -d

Ловушка: deploy.resources.limits в не-swarm compose

Секция deploy.resources.limits.memory молча игнорируется запуском через docker-compose up (это формат swarm). Лимит создаёт ложное ощущение защиты — что-то ограничивать нужно иначе (cgroups напрямую или mem_limit, зависят от версии).

Проверено 2026-09-26 (сборка balda-executor)

Дополнения по факту сборки Фазы 4:

Факт Значение
Сборка образа docker buildx build --builder arm64builder --platform linux/arm64 --provenance=false --load -t balda-executor:0.1.0 . — прошла, ~1 мин
compose.override.yaml при mounts: [] должен существовать обязательно: compose подхватывает его автоматически, а без файла up падает на монтировании
Имя каталога в выводе docker-compose пишет путь через нижний регистр (/Users/admin/docker/...) — macOS ФС регистронезависима, для человека разницы нет
Пересоздание контейнера docker-compose up -d после правки манифеста: Recreate → Started, 2-3 сек, --privileged не нужен
Отвязка проекта убрать запись из манифеста → render-mounts.py → up -d. Файлы на хосте целы (проверено: ls на хосте после отвязки)
Hardlink в тот же том ln -F даёт links=2 и одинаковый inode — проверено через stat -f "%l %i"
Hardlink на каталог API возвращает 400 до вызова ln (проверка is_dir()), ядро тоже откажет

Питфолл: psutil отсутствует в venv Eagle Dashboard

process/terminate-операции процесса Hermes (process kill) падают с No module named 'psutil'. Из-за этого supervisor не может трекать мост по PID, и для сервиса моста выбран health type: http, а не type: process — иначе карточка в дашборде всегда показывает «down», хотя сервис жив.

Проверка, что сервис под launchd действительно автозапускается

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.agents.bridge.plist
launchctl list | grep agents.bridge        # pid + last exit code
curl -s http://127.0.0.1:8790/healthz      # сервис отвечает

KeepAlive: true + RunAtLoad: true в plist → сервис переживает падение и перезагрузку. Требовать PATH в EnvironmentVariables обязательно: под launchd PATH минимальный, и /opt/homebrew/bin/docker иначе не находится.


Связанные заметки