Learning Platform
Глоссарий Troubleshooting
Урок 10.02 · 24 мин
Начальный
dockervolumesbind-mountmacosorbstack

Bind mounts: синтаксис, права, macOS-специфика

Bind mount — рабочая лошадка локальной разработки. На нём держится почти весь dev-workflow в контейнерах: редактируешь файл на хосте, контейнер видит изменение мгновенно. Но именно с bind mount связаны самые частые проблемы Junior’ов: «почему у файла owner = root», «почему npm install внутри контейнера пишет в host-папку с правами root», «почему bind на маке такой медленный». Разберём механику.


Два синтаксиса: -v и —mount

Исторически Docker предлагал короткий синтаксис через -v:

docker run -v $(pwd):/app python:3.13-slim
docker run -v /host/path:/container/path:ro alpine

Формат — host:container:options, через двоеточие. Удобно, но неоднозначно: если первая часть не начинается с /, Docker трактует её как имя named volume, а не как host path. Опечатался — получил пустой volume вместо bind.

Современный явный синтаксис — --mount:

docker run \
  --mount type=bind,source=$(pwd),target=/app \
  python:3.13-slim

Параметры через запятую, обязательно type=bind. Длиннее, но никаких сюрпризов. В compose-файлах для bind mount обычно используют именно длинную форму:

services:
  app:
    image: python:3.13-slim
    volumes:
      - type: bind
        source: ./src
        target: /app/src
TIP

Правило для скриптов: в compose — длинная форма (type: bind). В docker run для quick-experiments — короткая (-v), но всегда с абсолютным path через $(pwd), чтобы не получить случайный named volume.


Опции bind mount

Опция-v—mountЧто делает
read-only:roreadonlyКонтейнер не пишет в host
consistency:cachedconsistency=cachedHint для macOS (исторический, deprecated)
bind-propagation:sharedbind-propagation=sharedSubmount propagation (Linux only)
selinux:Z-SELinux-relabel (RHEL/Fedora)

Самый частый случай — read-only:

docker run --rm \
  -v $(pwd)/dags:/opt/airflow/dags:ro \
  apache/airflow:2.10.0 airflow dags list

Airflow видит DAG-папку, но не может туда писать. Это разумный default для production: ETL-код приложение не должно само себя переписывать.


Пользователи и группы — UID/GID и /etc/passwd

Владелец и права файлов

Здесь начинается самое больное. На Linux при bind mount Docker не меняет UID/GID файлов — что было на хосте, то и видно в контейнере.

Пример:

# На хосте я — UID 1000.
ls -ln /tmp/data
# -rw-r--r-- 1 1000 1000 ... file.txt

docker run --rm -v /tmp/data:/data alpine ls -ln /data
# -rw-r--r-- 1 1000 1000 ... file.txt

Внутри alpine процесс запущен от root (UID 0). Root всё равно может читать и писать. Но если образ запускает процесс от своего пользователя (что правильно для security), могут быть проблемы:

docker run --rm \
  -u 999:999 \
  -v /tmp/data:/data \
  alpine sh -c 'echo hi > /data/new.txt'
# Файл создан, owner = 999:999. Я (UID 1000) на хосте могу его прочитать, но не могу удалить, потому что я не владелец.
UID mismatch — почему он происходит
Host UID 501 (macOS)Стандартный пользовательский UID на macOS — 501. Соответствует первому admin-пользователю
bind mount
Container UID 1000Многие образы (Airflow, Postgres) внутри используют UID 1000 — это default first user в большинстве Linux distros
Файл создан в контейнереКонтейнер пишет файл от UID 1000. На macOS bind работает через VM, в которой Docker мапит UID — поэтому на маке UID часто 'правильный'. На Linux — буквальный mismatch
на хосте
На Linux: 'permission denied'С хоста (UID 501) попытка удалить или отредактировать файл — fail. На macOS чаще работает за счёт того, что VM прозрачно пробрасывает UID. На Linux это голая правда POSIX-прав

Решения:

  1. Запустить контейнер с UID хоста: -u $(id -u):$(id -g). Подходит для dev, не подходит для prod-образов с подготовленным user’ом.
  2. Chown через entrypoint.sh: скрипт при старте делает chown -R user:user /data и потом переходит на exec gosu user "$@". Стандартный паттерн в Postgres и Airflow.
  3. Совпадающий UID в образе и хосте. Build-arg UID=1000, в Dockerfile useradd -u $UID. На Linux совпадает в 90% случаев.

На macOS и Windows bind mount проходит через VM, и Docker Desktop / OrbStack обычно сглаживает UID-конфликты на лету (внутренняя реализация различается). Поэтому Junior, который разрабатывает на маке, не видит проблемы. На Linux-сервере она проявляется сразу.


macOS: почему bind медленный

Docker на macOS — это Linux VM под капотом. Контейнеры работают внутри этой VM. Host path /Users/me/project физически живёт на macOS, контейнерный путь — внутри VM. Чтобы файл с хоста стал виден в VM, нужен механизм проброса:

  • Docker Desktop: исторически osxfs (медленный), потом gRPC FUSE (быстрее), сейчас VirtioFS (самый быстрый из этой ветки). Включается в Settings, не везде по умолчанию.
  • OrbStack: свой механизм поверх VirtFS, заметно быстрее Docker Desktop.
  • Rancher Desktop / Lima: sshfs или 9p, медленнее обоих.
  • Mutagen: двунаправленная файловая синхронизация в файловую систему VM (не bind mount). Скорость нативная, но усложняет setup.
Bind mount на macOS — путь файла
VS Code пишет файлРедактор работает с обычной macOS-FS — APFS. Запись быстрая, локальная
File-share protocolVirtioFS / gRPC FUSE / 9p — протокол, который пробрасывает FS-операции в Linux VM. Каждая read/write — RPC через socket
Linux VM видит mountВнутри VM это выглядит как remote-FS. Все системные вызовы файлов транслируются обратно к хосту
Контейнер: bind mountКонтейнер получает кусок VM FS. Каждая операция: container -> VM kernel -> file-share -> macOS -> APFS. 4 hops vs 1 на Linux

Эффект: npm install или pip install в bind-папку на маке может занять 2-5 минут вместо 30 секунд на Linux. Решения:

  1. OrbStack. На M-серии маков это первый выбор. VirtFS-mount в OrbStack — самое близкое к нативной скорости, что есть на macOS.

  2. Держать node_modules / .venv в named volume. В compose делаем bind для кода и named volume для tяжёлых директорий:

    volumes:
      - ./src:/app/src
      - ./pyproject.toml:/app/pyproject.toml
      - venv:/app/.venv
  3. Mutagen / docker-sync. Двунаправленная синхронизация в named volume — даёт скорость VM-FS.

  4. :cached-хинт. Исторически Docker Desktop поддерживал :cached (контейнер может видеть устаревшую host-версию короткое время) и :delegated (хост может видеть устаревшую container-версию). После перехода на VirtioFS флаги стали no-op, но всё ещё валидны в синтаксисе.

NOTE

OrbStack на M-серии — это default для DE-разработки на маке. Запуск Postgres-стенда через bind mount в OrbStack vs Docker Desktop отличается в 3-5 раз по скорости I/O. Если ты сидишь на Docker Desktop и pipeline тормозит — переключись.


Когда bind mount — плохая идея

СценарийПочему bind плох
Postgres data в bind-папке на макеОчень медленно, плюс UID-конфликты, плюс fsync через VM bridge
Production-deploy с bind на host pathНе portable: новая машина — ручная подготовка папки и прав
Bind mount в multi-host swarm/k8sПапка на одной ноде, контейнер на другой — fail
Bind mount с node_modules/.venv на макеI/O bottleneck (тысячи мелких файлов)
Bind read-write конфигов от продаКонтейнер случайно перезапишет, на хосте остаются артефакты

Сценарии, где bind — правильный выбор:

СценарийПочему bind подходит
DAG-файлы Airflow на dev-машинеLive edit, scheduler подхватывает за секунды
Конфиг pgAdmin / AdminerОдин YAML, изменения сразу применяются
Test-fixtures для testcontainersТест прокидывает sample-CSV в контейнер
dbt-проект для локального rundbt run + редактирование SQL в IDE

Попробуй сам

# 1. Bind read-only — контейнер не пишет.
mkdir -p /tmp/ro-demo && echo "config" > /tmp/ro-demo/app.conf
docker run --rm \
  --mount type=bind,source=/tmp/ro-demo,target=/etc/app,readonly \
  alpine sh -c 'echo trying >> /etc/app/app.conf'
# -> sh: can't create /etc/app/app.conf: Read-only file system

# 2. UID mismatch (на Linux чувствуется явно).
mkdir -p /tmp/uid-demo
docker run --rm \
  -u 1500:1500 \
  -v /tmp/uid-demo:/data \
  alpine sh -c 'echo "from 1500" > /data/note.txt; ls -ln /data'
# На хосте:
ls -ln /tmp/uid-demo
# Видишь owner = 1500. На macOS Docker сглаживает, на Linux буквально.

# 3. Performance-тест на маке (если ты на маке).
docker run --rm -v $(pwd):/work alpine \
  sh -c 'time find /work -type f | wc -l'
# Сравни время с такой же командой на native-папке.
WARNING

Никогда не делай -v /:/host — кажется удобным «контейнер видит весь хост», но это полное снятие изоляции и прямой путь к рекурсивной катастрофе, если внутри запустится rm -rf.

В следующем уроке — named volumes: lifecycle, backup-стратегия, volume drivers.


Проверка знанийKnowledge check
На Linux-сервере ты запустил docker run -u 1000:1000 -v /opt/data:/data myapp . Внутри контейнера приложение пишет файлы в /data , но потом другой пользователь хоста (UID 1500) не может их редактировать. Что произошло и какие три способа это решить?
ОтветAnswer
Произошёл UID mismatch: bind mount не меняет owner'ов, контейнерный процесс (UID 1000) создал файлы с owner=1000, а пользователь хоста (UID 1500) не имеет прав на запись по POSIX-правилам. Три типичных решения: (1) запускать контейнер с UID хост-пользователя через -u $(id -u):$(id -g) — подходит для dev; (2) entrypoint.sh в образе, который делает chown на mount point при старте, а потом drop'ает privileges через gosu/su-exec — стандартный паттерн для Postgres, Airflow, Redis; (3) совпадающий UID между образом и хост-пользователем через build-arg в Dockerfile (ARG UID=1000; useradd -u $UID app). Также можно использовать named volume вместо bind — Docker сам выставит правильные права на volume.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём ключевая разница между синтаксисом -v и --mount для bind mount?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5