7.3. Bind mounts
Цели
После этого материала вы сможете:
- смонтировать каталог host в container и объяснить, чем это отличается от volume;
- предсказать, что произойдёт с файлами образа под точкой монтирования;
- диагностировать «пропажу» установленных пакетов при монтировании исходного кода;
- объяснить, почему bind mount отдельного файла ломается после правки редактором;
- применять
:ro,:zи:Zосознанно; - оценить риски монтирования сокета Docker.
Предварительные знания
- 7.1. Файловая система container;
- 7.2. Volumes — для сравнения поведения.
Ключевые термины
| Термин | Объяснение |
|---|---|
bind mount | Каталог или файл host, подключённый в container напрямую |
shadowing | Сокрытие содержимого образа точкой монтирования |
inode | Идентификатор файла в файловой системе; bind mount файла привязан к нему |
:z / :Z | Метки SELinux: общий и частный доступ |
propagation | Как монтирования распространяются между host и container |
Теория
Чем bind mount отличается от volume
| Volume | Bind mount | |
|---|---|---|
| Что монтируется | Каталог, созданный Docker | Существующий путь host |
| Указывается как | Имя | Абсолютный путь |
| Наполняется из образа | Да, если пуст | Никогда |
| Скрывает содержимое образа | Только если непуст | Всегда |
| Переносимость конфигурации | Высокая | Низкая |
| Docker управляет жизненным циклом | Да | Нет |
| Владелец и права | Из образа при наполнении | Из host, как есть |
| Производительность на Linux | Как у host | Как у host |
Две строки определяют почти все практические различия: bind mount всегда скрывает содержимое образа и никогда не наследует права из образа.
Shadowing: файлы образа исчезают
Монтирование накладывается поверх точки монтирования. Всё, что было в образе по этому пути, становится недоступным — не удаляется, а перестаёт быть видимым.
Образ: /app/main.py, /app/.venv/, /app/lib/
Монтируем: ./src → /app
Container видит: только содержимое ./src
Для volume это происходит лишь когда volume непуст. Для bind mount — всегда, включая монтирование пустого каталога: тогда container увидит пустоту на месте файлов образа.
Симптом в Python-проекте узнаваемый:
ModuleNotFoundError: No module named 'fastapi'
Пакет установлен в образе, сборка прошла, но виртуальное окружение оказалось внутри смонтированного каталога и было скрыто.
Решение — держать окружение вне монтируемого пути:
RUN python -m venv /opt/venv # НЕ /app/.venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
Тогда -v "$PWD:/app" скрывает только код — который и должен приходить с host, — а окружение остаётся на месте.
Альтернатива, когда окружение обязано быть внутри /app: закрыть его anonymous volume поверх bind mount.
docker run -v "$PWD:/app" -v /app/.venv ...
Второе монтирование глубже первого, поэтому побеждает: /app приходит с host, а /app/.venv — из образа. Приём работает, но добавляет anonymous volume при каждом запуске (урок 7.2); первый вариант чище.
Абсолютные пути и переносимость
Bind mount требует абсолютного пути. docker run не разворачивает относительные — их разворачивает shell:
docker run -v "$PWD/data:/data" ... # shell подставит абсолютный путь
docker run -v ./data:/data ... # ошибка или неожиданный результат
В Compose иначе: относительные пути разрешаются от каталога Compose-файла, и это делает конфигурацию переносимой:
services:
app:
volumes:
- ./src:/app/src # относительно расположения compose.yaml
Отсюда практическое правило: bind mounts описывают в Compose, а не в командах docker run. В Compose путь остаётся относительным и переносится вместе с репозиторием.
Bind mount отдельного файла
Монтировать можно не только каталог:
docker run -v "$PWD/config.ini:/app/config.ini:ro" ...
Здесь скрывается ровно один файл. Но у такого монтирования есть неочевидное свойство: привязка идёт к inode, а не к имени.
Большинство редакторов сохраняют файл не записью на месте, а созданием нового и переименованием. Новый файл получает новый inode — а bind mount остался привязан к старому. Container продолжает видеть прежнее содержимое.
| Способ правки | Inode | Container видит изменения |
|---|---|---|
echo ... >> file | Прежний | Да |
sed -i без резервной копии | Новый | Нет |
| Сохранение из vim, VS Code | Обычно новый | Нет |
cp new file | Прежний | Да |
Отсюда правило: монтируйте каталог, а не файл. Каталог не привязан к inode отдельного файла, и правки видны всегда.
Если файл монтировать необходимо, изменения применяются перезапуском container'а.
Read-only
docker run --mount type=bind,source=/etc/app,target=/etc/app,readonly ...
docker run -v /etc/app:/etc/app:ro ...
Флаг стоит ставить по умолчанию для всего, что container не должен изменять: конфигурация, сертификаты, справочные данные. Он превращает ошибку в приложении в явный отказ вместо порчи файлов host.
Ограничение: read-only относится к монтированию, а не к host. Процесс с достаточными правами на host изменит файлы независимо от флага.
SELinux: :z и :Z
На системах с SELinux (Fedora, RHEL, CentOS, Rocky) bind mount без метки даёт Permission denied даже при верных правах Unix — доступ блокирует политика SELinux.
| Суффикс | Что делает | Когда |
|---|---|---|
:z | Общая метка: каталог доступен нескольким container'ам | Общие данные |
:Z | Частная метка: каталог доступен только этому container'у | Данные одного container'а |
docker run -v /srv/data:/data:Z ...
Предупреждение. Суффикс изменяет метки SELinux рекурсивно и необратимо для исходного каталога. Применение :Z к системному каталогу вроде /home или /usr может нарушить работу host. Метки ставят только на каталоги, созданные для этой цели.
На Ubuntu и Debian с AppArmor суффиксы не нужны и игнорируются.
Propagation
Опция управляет тем, увидит ли container монтирования, выполненные на host внутри примонтированного каталога, и наоборот.
| Режим | Поведение |
|---|---|
rprivate (по умолчанию) | Монтирования не распространяются ни в одну сторону |
rshared | Распространяются в обе стороны |
rslave | Host → container |
Значение по умолчанию подходит почти всегда. rslave нужен, когда host монтирует что-то внутрь уже примонтированного каталога и container должен это увидеть — например, при работе с внешними дисками.
Сокет Docker
docker run -v /var/run/docker.sock:/var/run/docker.sock ...
Приём применяется для CI-агентов и инструментов, которым нужно управлять container'ами.
Это эквивалент выдачи root на host. Процесс с доступом к сокету может запустить container с --privileged и монтированием /, то есть получить полный контроль над машиной. Никакой изоляции здесь нет.
Допустимо в средах, где вы и так доверяете коду с правами администратора. Недопустимо для кода, полученного извне. Альтернативы разбираются в разделе 12.
Внутренний механизм
Что делает ядро
Bind mount — стандартная возможность Linux:
mount --bind /source /target
Ядро добавляет запись в таблицу монтирований mount namespace container'а. Файловая система одна и та же — доступ идёт напрямую, без промежуточного слоя.
Отсюда два следствия. Первое: на Linux bind mount не имеет накладных расходов — это тот же путь к тем же данным. Второе: содержимое, скрытое точкой монтирования, никуда не делось. Оно перестало быть достижимым по этому пути в этом namespace (урок 2.3).
Как убедиться, что файлы образа целы
Скрытые файлы можно увидеть, запустив container без монтирования, — образ не изменился. Это же даёт способ диагностики: сравнить ls с монтированием и без.
Команды и примеры
Bind mount скрывает файлы образа
docker build -q -t shadowdemo - <<'EOF' > /dev/null
FROM alpine:3.21
RUN mkdir -p /app && \
echo "из образа" > /app/from-image.txt && \
echo "тоже из образа" > /app/second.txt
CMD ["sleep", "300"]
EOF
mkdir -p /tmp/shadow-src && echo "с host" > /tmp/shadow-src/from-host.txt
echo "═══ без монтирования ═══"
docker run --rm shadowdemo ls -1 /app
echo "═══ с bind mount ═══"
docker run --rm -v /tmp/shadow-src:/app shadowdemo ls -1 /app
echo "═══ с монтированием ПУСТОГО каталога ═══"
mkdir -p /tmp/shadow-empty
docker run --rm -v /tmp/shadow-empty:/app shadowdemo sh -c 'ls -A /app | wc -l | xargs echo "файлов:"'
echo "═══ для сравнения: пустой volume ═══"
docker volume create shadow-vol > /dev/null
docker run --rm -v shadow-vol:/app shadowdemo ls -1 /app
docker volume rm shadow-vol > /dev/null
Ожидаемый вывод:
═══ без монтирования ═══
from-image.txt
second.txt
═══ с bind mount ═══
from-host.txt
═══ с монтированием ПУСТОГО каталога ═══
файлов: 0
═══ для сравнения: пустой volume ═══
from-image.txt
second.txt
Четыре блока показывают разницу исчерпывающе. Bind mount скрывает файлы образа всегда — даже пустой каталог даёт пустоту. Пустой volume, наоборот, наполняется файлами образа (урок 7.2).
Файлы образа при этом целы: первый блок выполнялся с тем же образом.
Классическая ошибка Python-проекта
mkdir -p /tmp/venv-trap/src && cd /tmp/venv-trap
cat > src/app.py <<'PY'
import sys
try:
import click
print(f"click {click.__version__} найден")
except ModuleNotFoundError as exc:
print(f"ОШИБКА: {exc}")
sys.exit(1)
PY
cat > requirements.txt <<'EOF'
click==8.3.0
EOF
echo "═══ вариант A: venv внутри /app ═══"
cat > Dockerfile.bad <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /app/.venv && /app/.venv/bin/pip install -q -r requirements.txt
ENV PATH="/app/.venv/bin:$PATH"
COPY src/ ./src/
CMD ["python", "src/app.py"]
EOF
docker build -q -f Dockerfile.bad -t venv-bad . > /dev/null
printf ' без монтирования: '
docker run --rm venv-bad
printf ' с bind mount: '
docker run --rm -v "$PWD/src:/app/src" venv-bad
printf ' монтируем весь /app: '
docker run --rm -v "$PWD:/app" venv-bad || true
echo
echo "═══ вариант B: venv в /opt/venv ═══"
cat > Dockerfile.good <<'EOF'
FROM python:3.13-slim
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY requirements.txt .
RUN pip install -q -r requirements.txt
COPY src/ ./src/
CMD ["python", "src/app.py"]
EOF
docker build -q -f Dockerfile.good -t venv-good . > /dev/null
printf ' без монтирования: '
docker run --rm venv-good
printf ' монтируем весь /app: '
docker run --rm -v "$PWD:/app" venv-good
Ожидаемый вывод:
═══ вариант A: venv внутри /app ═══
без монтирования: click 8.3.0 найден
с bind mount: click 8.3.0 найден
монтируем весь /app: ОШИБКА: No module named 'click'
═══ вариант B: venv в /opt/venv ═══
без монтирования: click 8.3.0 найден
с bind mount: click 8.3.0 найден
монтируем весь /app: click 8.3.0 найден
Третья строка первого блока — самая частая ошибка в разработке с Docker. Пакет установлен, образ собран правильно, но /app/.venv скрыт монтированием, и PATH указывает в пустоту.
Обратите внимание на вторую строку: монтирование только src/ проблемы не вызывает — окружение лежит выше по дереву. Ошибка проявляется лишь при монтировании всего /app, что и делают обычно.
Вариант B устойчив к обоим способам монтирования: /opt/venv вне /app и скрыть его нельзя.
Приём с anonymous volume
Если окружение обязано находиться в /app:
cd /tmp/venv-trap
printf ' bind mount + anonymous volume поверх: '
docker run --rm -v "$PWD:/app" -v /app/.venv venv-bad
echo " созданных anonymous volumes:"
docker volume ls -q | grep -cE '^[0-9a-f]{64}$' | sed 's/^/ /'
Ожидаемый вывод:
bind mount + anonymous volume поверх: click 8.3.0 найден
созданных anonymous volumes:
1
Приём работает: более глубокое монтирование побеждает. Цена — anonymous volume на каждый запуск без --rm, который придётся убирать (урок 7.2).
docker volume prune -f > /dev/null
docker rmi -f venv-bad venv-good shadowdemo > /dev/null 2>&1
Bind mount файла и правка редактором
mkdir -p /tmp/filemount && cd /tmp/filemount
echo "version=1" > config.ini
docker run -d --name filemnt \
-v "$PWD/config.ini:/app/config.ini:ro" \
alpine:3.21 sh -c 'while true; do sleep 1; done' > /dev/null
printf 'исходно: %s\n' "$(docker exec filemnt cat /app/config.ini)"
echo "═══ дописывание в конец (inode сохраняется) ═══"
inode_before="$(stat -c '%i' config.ini)"
echo "extra=yes" >> config.ini
printf ' inode: %s → %s\n' "$inode_before" "$(stat -c '%i' config.ini)"
printf ' container видит: %s\n' "$(docker exec filemnt sh -c 'tr "\n" " " < /app/config.ini')"
echo "═══ правка через sed -i (создаётся новый файл) ═══"
inode_before="$(stat -c '%i' config.ini)"
sed -i 's/version=1/version=2/' config.ini
printf ' inode: %s → %s\n' "$inode_before" "$(stat -c '%i' config.ini)"
printf ' на host: %s\n' "$(head -1 config.ini)"
printf ' container видит: %s ← не обновилось\n' "$(docker exec filemnt head -1 /app/config.ini)"
docker rm -f filemnt > /dev/null
Ожидаемый вывод:
исходно: version=1
═══ дописывание в конец (inode сохраняется) ═══
inode: 1443521 → 1443521
container видит: version=1 extra=yes
═══ правка через sed -i (создаётся новый файл) ═══
inode: 1443521 → 1443893
на host: version=2
container видит: version=1 ← не обновилось
Расхождение видно прямо: на host version=2, в container'е version=1. Причина в изменившемся inode — bind mount остался привязан к старому файлу, который ещё существует, пока его держит монтирование.
Теперь то же самое с монтированием каталога:
cd /tmp/filemount
docker run -d --name dirmnt -v "$PWD:/app:ro" \
alpine:3.21 sh -c 'while true; do sleep 1; done' > /dev/null
printf 'до правки: %s\n' "$(docker exec dirmnt head -1 /app/config.ini)"
sed -i 's/version=2/version=3/' config.ini
printf 'после sed: %s ← обновилось\n' "$(docker exec dirmnt head -1 /app/config.ini)"
docker rm -f dirmnt > /dev/null
cd /tmp && rm -rf /tmp/filemount /tmp/venv-trap /tmp/shadow-src /tmp/shadow-empty
Ожидаемый вывод:
до правки: version=2
после sed: version=3 ← обновилось
Монтирование каталога не привязано к inode отдельного файла, поэтому видит любую правку. Это и есть причина рекомендации монтировать каталоги.
Read-only и куда он не распространяется
mkdir -p /tmp/ro-demo && echo "исходные данные" > /tmp/ro-demo/data.txt
echo "═══ запись из container ═══"
docker run --rm --mount type=bind,source=/tmp/ro-demo,target=/data,readonly \
alpine:3.21 sh -c 'echo изменено > /data/data.txt' 2>&1 | tail -1
echo "═══ файл на host не изменился ═══"
cat /tmp/ro-demo/data.txt
echo "═══ вложенное монтирование на запись ═══"
mkdir -p /tmp/ro-demo/writable
docker run --rm \
--mount type=bind,source=/tmp/ro-demo,target=/data,readonly \
--mount type=bind,source=/tmp/ro-demo/writable,target=/data/writable \
alpine:3.21 sh -c 'echo можно > /data/writable/ok.txt && echo "во вложенный каталог запись прошла"'
cat /tmp/ro-demo/writable/ok.txt
rm -rf /tmp/ro-demo
Ожидаемый вывод:
═══ запись из container ═══
sh: can't create /data/data.txt: Read-only file system
═══ файл на host не изменился ═══
исходные данные
═══ вложенное монтирование на запись ═══
во вложенный каталог запись прошла
можно
Третий блок показывает важное: readonly относится к конкретному монтированию, а не ко всему поддереву. Более глубокое монтирование задаёт свои права. Это регулярно используют — read-only корень плюс отдельные каталоги на запись (урок 7.4).
Диагностика: что скрыто монтированием
docker build -q -t diag-shadow - <<'EOF' > /dev/null
FROM python:3.13-slim
RUN mkdir -p /app && echo "print('из образа')" > /app/entry.py && \
mkdir -p /app/lib && echo "x = 1" > /app/lib/helper.py
CMD ["sleep", "300"]
EOF
mkdir -p /tmp/diag-src && echo "print('с host')" > /tmp/diag-src/entry.py
echo "═══ что в образе по пути /app ═══"
docker run --rm diag-shadow find /app -type f | sort
echo "═══ что видно с монтированием ═══"
docker run --rm -v /tmp/diag-src:/app diag-shadow find /app -type f | sort
echo "═══ разница = скрытое ═══"
diff <(docker run --rm diag-shadow find /app -type f | sort) \
<(docker run --rm -v /tmp/diag-src:/app diag-shadow find /app -type f | sort) \
| grep '^<' | sed 's/^< / скрыто: /'
docker rmi -f diag-shadow > /dev/null; rm -rf /tmp/diag-src
Ожидаемый вывод:
═══ что в образе по пути /app ═══
/app/entry.py
/app/lib/helper.py
═══ что видно с монтированием ═══
/app/entry.py
═══ разница = скрытое ═══
скрыто: /app/lib/helper.py
Такое сравнение — самый быстрый ответ на вопрос «куда делся файл». Два запуска одного образа, с монтированием и без, и diff списков.
Заметьте, что /app/entry.py присутствует в обоих списках, но это разные файлы: в первом случае из образа, во втором с host. Сравнение по именам этого не покажет — при подозрении на подмену сравнивают содержимое.
Практическое упражнение
Задание. Организуйте разработку Python-приложения с bind mount, устойчивую к четырём проблемам.
- Правка кода на host видна в container без пересборки.
- Установленные в образе пакеты не скрываются монтированием — доказать.
- Конфигурация монтируется read-only, и правка её редактором видна container'у.
- Каталог кэша приложения не засоряет репозиторий на host.
- Одна команда запускает всё; конфигурация переносима (без абсолютных путей).
Каждый пункт подтвердите проверкой.
Подсказки
Подсказка 1
Пункт 2 решается расположением виртуального окружения, а не флагами монтирования.
Подсказка 2
Пункт 3 требует монтировать каталог с конфигурацией, а не сам файл.
Подсказка 3
Пункт 4: то, что не должно попадать на host, монтируют volume или tmpfs поверх bind mount.
Подсказка 4
Пункт 5 — про Compose: там пути относительны каталогу файла.
Решение
Показать решение
mkdir -p /tmp/devmount/src/app /tmp/devmount/config && cd /tmp/devmount
cat > src/app/__init__.py <<'PY'
"""Приложение для демонстрации bind mount при разработке."""
PY
cat > src/app/main.py <<'PY'
"""Проверяет всё, что требуется от окружения разработки."""
from __future__ import annotations
import os
import sys
from pathlib import Path
CACHE_DIR = Path(os.environ.get("APP_CACHE", "/app/.cache"))
CONFIG = Path(os.environ.get("APP_CONFIG", "/app/config/app.ini"))
VERSION = "1" # правится на host для проверки пункта 1
def main() -> int:
# Пункт 2: пакет из образа должен быть доступен
try:
import click
pkg = f"click {click.__version__}"
except ModuleNotFoundError as exc:
print(f" пакет: ОШИБКА — {exc}")
return 1
# Пункт 3: конфигурация читается с host
config_line = CONFIG.read_text().strip().splitlines()[0] if CONFIG.exists() else "ОТСУТСТВУЕТ"
# Пункт 4: кэш пишется, но не в репозиторий host
CACHE_DIR.mkdir(parents=True, exist_ok=True)
(CACHE_DIR / "marker.txt").write_text("кэш приложения\n")
print(f" версия: {VERSION}")
print(f" пакет: {pkg}")
print(f" конфиг: {config_line}")
print(f" кэш: {CACHE_DIR} (записан)")
return 0
if __name__ == "__main__":
sys.exit(main())
PY
cat > config/app.ini <<'EOF'
setting=исходное
EOF
cat > requirements.txt <<'EOF'
click==8.3.0
EOF
cat > .dockerignore <<'EOF'
.git
.venv
__pycache__
*.py[cod]
.cache
EOF
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/opt/venv/bin:$PATH"
# Пункт 2: окружение ВНЕ /app — bind mount его не скроет
RUN python -m venv /opt/venv
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
COPY src/ ./src/
ENV PYTHONPATH=/app/src
CMD ["python", "-m", "app.main"]
EOF
cat > compose.yaml <<'EOF'
services:
dev:
build: .
environment:
APP_CONFIG: "/app/config/app.ini"
APP_CACHE: "/app/.cache"
volumes:
# Пункт 1: код с host, пути относительны этому файлу (пункт 5)
- ./src:/app/src
# Пункт 3: каталог, а не файл — правка редактором будет видна
- ./config:/app/config:ro
# Пункт 4: кэш в volume поверх bind mount, на host не попадает
- dev-cache:/app/.cache
command: ["sh", "-c", "while true; do sleep 1; done"]
volumes:
dev-cache:
EOF
# ── Проверка ──
docker compose up -d --build > /dev/null 2>&1
sleep 3
echo "═══ Пункты 2, 3, 4: базовый запуск ═══"
docker compose exec -T dev python -m app.main
echo
echo "═══ Пункт 1: правка кода без пересборки ═══"
sed -i 's/^VERSION = "1"/VERSION = "2-ПРАВКА-НА-HOST"/' src/app/main.py
docker compose exec -T dev python -m app.main | grep версия
echo
echo "═══ Пункт 2: доказательство, что окружение не скрыто ═══"
printf ' venv в образе: %s\n' "$(docker compose exec -T dev sh -c 'ls -d /opt/venv')"
printf ' click доступен: %s\n' "$(docker compose exec -T dev python -c 'import click; print("да")')"
printf ' /app/src с host: %s\n' \
"$(docker compose exec -T dev sh -c 'stat -c %Y /app/src/app/main.py')"
echo
echo "═══ Пункт 3: правка конфигурации редактором (новый inode) ═══"
printf ' inode до: %s\n' "$(stat -c '%i' config/app.ini)"
sed -i 's/setting=исходное/setting=ИЗМЕНЕНО-РЕДАКТОРОМ/' config/app.ini
printf ' inode после: %s\n' "$(stat -c '%i' config/app.ini)"
docker compose exec -T dev python -m app.main | grep конфиг
echo
echo "═══ Пункт 3: конфигурация недоступна на запись ═══"
docker compose exec -T dev sh -c 'echo x > /app/config/app.ini' 2>&1 | tail -1 | sed 's/^/ /'
echo
echo "═══ Пункт 4: кэш не попал в репозиторий host ═══"
printf ' в container: %s\n' "$(docker compose exec -T dev sh -c 'ls /app/.cache')"
printf ' на host: %s\n' "$(ls .cache 2>/dev/null || echo 'каталога нет — верно')"
echo
echo "═══ Пункт 5: переносимость ═══"
grep -c '/tmp/devmount\|/home/' compose.yaml | xargs printf ' абсолютных путей в compose.yaml: %s\n'
docker compose down -v > /dev/null 2>&1
cd /tmp && rm -rf /tmp/devmount
Ожидаемый вывод:
═══ Пункты 2, 3, 4: базовый запуск ═══
версия: 1
пакет: click 8.3.0
конфиг: setting=исходное
кэш: /app/.cache (записан)
═══ Пункт 1: правка кода без пересборки ═══
версия: 2-ПРАВКА-НА-HOST
═══ Пункт 2: доказательство, что окружение не скрыто ═══
venv в образе: /opt/venv
click доступен: да
/app/src с host: 1785412843
═══ Пункт 3: правка конфигурации редактором (новый inode) ═══
inode до: 1443521
inode после: 1443902
конфиг: setting=ИЗМЕНЕНО-РЕДАКТОРОМ
═══ Пункт 3: конфигурация недоступна на запись ═══
sh: can't create /app/config/app.ini: Read-only file system
═══ Пункт 4: кэш не попал в репозиторий host ═══
в container: marker.txt
на host: каталога нет — верно
═══ Пункт 5: переносимость ═══
абсолютных путей в compose.yaml: 0
Все пять пунктов выполнены.
Три решения, определяющие качество.
/opt/venv вместо /app/.venv — весь пункт 2 держится на этом. Никакие флаги монтирования не помогли бы: bind mount на /app скрывает всё, что под ним. Проблема решается расположением, а не настройкой. Это общий принцип: то, что приходит из образа, должно лежать вне монтируемых путей.
Монтируется ./config, а не ./config/app.ini. Вывод показывает, что inode изменился после sed -i — при монтировании файла container увидел бы старую версию. Каталог к inode отдельного файла не привязан, и правка любым редактором видна сразу.
Кэш закрыт named volume поверх bind mount. Приложение пишет в /app/.cache, но /app/.cache — отдельное монтирование, глубже /app/src. На host ничего не появляется, .gitignore для этого не нужен, а кэш переживает перезапуск (в отличие от tmpfs). Named volume вместо anonymous выбран сознательно: его видно в docker volume ls и удаляет он compose down -v.
Чего решение не делает. Файлы, созданные приложением в /app/src, будут принадлежать пользователю container'а — если это root, на host они окажутся root-овыми и не удалятся обычным пользователем. Здесь этого не видно, потому что приложение в src/ не пишет. Полное решение проблемы — в уроке 7.5.
Проверка результата
mkdir -p /tmp/bm-check && echo host > /tmp/bm-check/f.txt
docker run --rm -v /tmp/bm-check:/d:ro alpine:3.21 sh -c 'cat /d/f.txt; echo x > /d/f.txt' 2>&1 | tail -2
rm -rf /tmp/bm-check
Ожидается вывод host и отказ записи Read-only file system.
Типичные ошибки
| Ошибка | Причина | Исправление |
|---|---|---|
ModuleNotFoundError при монтировании кода | Окружение внутри монтируемого пути | Venv в /opt/venv вне /app |
| Монтирование одного файла конфигурации | Кажется точнее | Правка редактором меняет inode; монтировать каталог |
Относительный путь в docker run -v | Работает в Compose | Использовать "$PWD/..." или Compose |
| Абсолютные пути в Compose | Скопировали из docker run | Относительные пути переносимы |
:Z на системном каталоге | Скопировали из инструкции | Метки меняются рекурсивно; можно сломать host |
| Ожидают наполнения из образа | Путают с volume | Bind mount никогда не наполняется |
-v с опечаткой в пути | Позиционный синтаксис | Docker создаст пустой каталог; --mount даст ошибку |
| Монтирование сокета Docker «для удобства» | Нужен доступ к API | Эквивалент root на host |
| Bind mount для данных базы | Кажется прозрачнее | Права и переносимость; volume лучше |
Забывают :ro для конфигурации | Не подумали | Приложение может испортить файлы host |
Контрольные вопросы
На понимание:
- Почему bind mount скрывает файлы образа, а пустой volume — нет?
- Почему
sed -iпо смонтированному файлу не виден в container'е? - Чем
:zотличается от:Zи почему их применяют осторожно? - Распространяется ли
readonlyна вложенные монтирования? - Почему монтирование сокета Docker эквивалентно выдаче root?
На применение:
- Как смонтировать код разработки, не потеряв установленные пакеты?
- Как сделать конфигурацию Compose переносимой между машинами?
- Как узнать, какие файлы образа скрыты монтированием?
На диагностику:
- После
docker compose upприложение не находит зависимость. Порядок действий? - Правка файла на host не видна в container'е. Две версии?
Краткое резюме
- Bind mount подключает существующий путь host напрямую, без участия Docker.
- Он всегда скрывает содержимое образа по этому пути, включая пустой каталог.
- Файлы образа при этом целы — они недоступны, а не удалены.
- Виртуальное окружение держат вне монтируемого пути:
/opt/venv, не/app/.venv. - Более глубокое монтирование побеждает:
-v /app/.venvвернёт окружение из образа. - Bind mount файла привязан к inode; правка редактором создаёт новый и рвёт связь.
- Монтируйте каталоги, а не отдельные файлы.
docker runтребует абсолютных путей, Compose разрешает относительные от своего каталога.:roотносится к конкретному монтированию; вложенные задают права сами.:zи:Zнужны на системах с SELinux и меняют метки рекурсивно.- На Linux bind mount не имеет накладных расходов — это тот же путь к тем же данным.
- Монтирование
/var/run/docker.sockравносильно выдаче прав root на host.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Docker: bind mounts | https://docs.docker.com/engine/storage/bind-mounts/ | Поведение, shadowing, -v против --mount |
| Docker: storage overview | https://docs.docker.com/engine/storage/ | Сравнение механизмов |
Docker: --mount reference | https://docs.docker.com/reference/cli/docker/container/run/#mount | Опции readonly, bind-propagation |
| Compose: volumes | https://docs.docker.com/reference/compose-file/services/#volumes | Относительные пути, короткий и длинный синтаксис |
| Docker: SELinux labels | https://docs.docker.com/engine/storage/bind-mounts/#configure-the-selinux-label | :z и :Z |
Linux: mount --bind | https://man7.org/linux/man-pages/man8/mount.8.html | Механизм ядра, propagation |
| Docker: security | https://docs.docker.com/engine/security/ | Риски доступа к сокету демона |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → tmpfs и read-only filesystem
Главное оглавление