12 KiB
Executable File
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.
4. Hardlink — чего он НЕ может (независимо от Docker)
Это свойство ФС, не Docker:
- Hardlink на каталог не делается. Ни в APFS, ни в ext4, ни в NTFS.
ln dir1 dir2→Operation not permitted. У каталога появилась бы ссылка на себя → цикл в дереве. Запрещено на уровне syscall. - Между томами не работает. Только внутри одной ФС (один
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. Что делать в реальном проекте
- Внешние каталоги — только через
-vв compose +docker compose up -d. Манифест монтажей (списокname+host_path) генерируетcompose.override.yaml— composefile не правится скриптом вручную. - auth/секреты CLI — на хосте, ВНЕ рабочего каталога агента. Иначе агент,
которому рабочий каталог смонтирован на запись, получает доступ к своим же
токенам. Монтировать отдельным
-v. - Шары —
rsyncв локальную папку, не прямой mount. - Hardlink — только файлы внутри одного тома, с проверкой
st_dev. - Не строить архитектуру на 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, зависят от версии).
Связанные заметки
- personal/plans/balda-mm-phase4-agents — применение этих выводов (Фаза 4)
- personal/projects/balda/balda-mm-delegation-plan — общий план проекта
- скилл
eagle-dashboard— сервис-реестр, health-типы, launchd - скилл
docker-service-administration— lifecycle контейнеров