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

251 lines
15 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-хоста:
```bash
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:
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. Команды воспроизведения
```bash
# 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/` |
```bash
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-сборок использовать именованный билдер:
```bash
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 действительно автозапускается
```bash
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` иначе не находится.
---
## Связанные заметки
- [[personal/plans/balda-mm-phase4-agents]] — применение этих выводов (Фаза 4)
- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план проекта
- скилл `eagle-dashboard` — сервис-реестр, health-типы, launchd
- скилл `docker-service-administration` — lifecycle контейнеров