Docker Compose: bind mount или named volume
Разбираем разницу между bind mount и named volume в Docker Compose. Узнайте, что выбрать для конфигураций и баз данных, как избежать проблем с правами доступа и переносить данные.
Bind mount или named volume: краткий ответ
В Docker Compose существует два типа томов, и выбор зависит от того, кто управляет файлами. Используйте bind mount для файлов, которые вы редактируете и читаете самостоятельно: конфигураций, шаблонов, статических сайтов. Используйте named volume для данных, которыми управляет само приложение: файлов баз данных, поисковых индексов, загруженного медиаконтента. Bind mount указывает на путь в хост-системе, который можно открыть в текстовом редакторе. Named volume — это хранилище, которое Docker создает и отслеживает самостоятельно; доступ к нему осуществляется через Docker.
Оба типа указываются в одной и той же секции volumes: внутри сервиса, поэтому их часто путают. Разница заключается в левой части от двоеточия. Если левая часть начинается с . или /, это путь на хосте, то есть bind mount. Все остальное — это имя, то есть named volume, и это имя должно быть дополнительно объявлено в блоке volumes: верхнего уровня.
Два синтаксиса в файле compose
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: nginx:1.27
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site:/usr/share/nginx/html:ro
volumes:
pgdata:pgdata:/var/lib/postgresql/data — это именованный том. ./nginx.conf:/etc/nginx/nginx.conf — это bind mount, а :ro монтирует его в режиме только для чтения, что является правильным значением по умолчанию для конфигурации, которую контейнер не должен изменять. Если забыть запись volumes: верхнего уровня, Compose завершит работу с ошибкой service "db" refers to undefined volume pgdata.
Запустите проект и посмотрите, что создал Docker:
docker compose up -d
docker volume lsТом называется не pgdata. Он называется <project>_pgdata, где имя проекта по умолчанию совпадает с именем каталога, содержащего файл compose. Каталог с именем myapp дает myapp_pgdata. Это важно, так как переименование каталога приведет к созданию нового пустого тома, и будет казаться, что приложение потеряло данные. Это не так: старый том по-прежнему отображается в выводе docker volume ls. Зафиксируйте имя с помощью name: в файле compose или установите COMPOSE_PROJECT_NAME, если каталог может быть перемещен. Подобные настройки следует размещать вместе с другими файлами окружения и секретами Compose.
Почему ошибки прав доступа возникают только при использовании bind mounts
Это самое важное практическое различие, которое вытекает из одного правила: именованный том (named volume), пустой при первом использовании, заполняется данными из образа, а bind mount — никогда.
Когда Docker монтирует пустой именованный том поверх директории, которая уже содержит данные в образе, он копирует это содержимое в том, сохраняя владельца и права доступа, заданные в образе. Официальный образ Postgres поставляется с /var/lib/postgresql/data, владельцем которого является его собственный пользователь postgres, поэтому том получает того же владельца с тем же числовым идентификатором, и база данных запускается.
Bind mount работает иначе. Контейнер видит всё, что находится на хосте, включая владельца, а содержимое образа по этому пути скрывается. Если директория на хосте не существует, Docker daemon создаёт её. Поскольку демон работает от имени root, вы получаете директорию, владельцем которой является root:root. Процесс в контейнере, запущенный от имени пользователя без прав root, не может писать в неё:
PermissionError: [Errno 13] Permission denied: '/data/app.db'Решение заключается в том, чтобы привести идентификаторы в соответствие. Владение через bind mount сравнивается по числовому идентификатору пользователя (uid), а не по имени, так как у контейнера свой собственный /etc/passwd. Пользователь с именем app внутри контейнера ничего не значит для хоста. Uid 1000 означает uid 1000 с обеих сторон.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id выводит uid, от имени которого фактически запущен процесс в контейнере. Приведите владельца директории на хосте к этому числу или закрепите за контейнером ваш идентификатор с помощью user: "1000:1000" в описании сервиса. Закрепление user: — более чистое решение для приложения, которое вы написали самостоятельно. Изменение владельца директории на хосте через chown безопаснее для образа, который писали не вы, так как некоторые образы запускают entrypoint от root, сбрасывают привилегии и ожидают наличия специфических прав доступа внутри.
Стоит знать ещё о двух ловушках. В Fedora, RHEL и других системах с активным SELinux (security-enhanced Linux) доступ к bind mount запрещён, пока не будет изменён контекст безопасности. Добавьте :z для пути, разделяемого между контейнерами, или :Z для пути, который должен использовать только один контейнер, записывая это как - ./data:/data:Z. Кроме того, bind mount отдельного файла, а не директории, перестаёт работать, если редактор заменяет файл целиком вместо перезаписи содержимого, так как точка монтирования привязана к исходному inode. Контейнер продолжит видеть старое содержимое до перезапуска. Монтируйте родительскую директорию, если файл часто редактируется.
Производительность: где разница ощутима
На сервере под управлением Linux оба типа хранилищ используют один и тот же путь в ядре, поэтому разница в пропускной способности незначительна, и этот фактор не должен быть решающим при выборе. Именованные тома, использующие драйвер по умолчанию local, располагаются в той же файловой системе, что и остальные данные Docker, в директории /var/lib/docker/volumes/, тогда как bind mount находится там, куда вы его направили.
Разница проявляется в Docker Desktop для macOS и Windows, где контейнеры работают внутри виртуальной машины. В этом случае bind mount проходит из файловой системы хоста в виртуальную машину через уровень совместного доступа к файлам, и рабочие нагрузки с большим количеством мелких операций ввода-вывода, такие как дерево зависимостей Node.js или кэш PHP-фреймворка, заметно замедляются. Именованные тома остаются внутри виртуальной машины и не несут этих издержек. Именно поэтому во многих compose-файлах для разработки исходный код подключается через bind mount, а директория node_modules объявляется как именованный том.
Другое реальное различие заключается в том, где физически хранятся данные. Bind mount в /mnt/backup помещает данные на соответствующий диск. Именованный том размещается в той файловой системе, где находится /var/lib/docker, что на VPS обычно является корневым диском. База данных, растущая внутри именованного тома, заполняет тот же диск, на котором хранятся системные логи. Проверьте это, прежде чем возникнет инцидент:
docker system df -v
df -h /var/lib/dockerКоманда docker system df -v выводит список всех томов с указанием их размера и отмечает те, на которые больше не ссылается ни один контейнер.
Просмотр именованного тома
Именованный том не является «черным ящиком». Узнайте у Docker, где он находится:
docker volume inspect myapp_pgdataПоле Mountpoint содержит реальный путь на хосте, обычно это /var/lib/docker/volumes/myapp_pgdata/_data. Вы можете прочитать его с помощью sudo ls, что полезно для быстрой проверки. Не рассматривайте это место как подходящее для редактирования файлов. Запись туда от имени root приводит к повторному возникновению проблем с правами владения, описанных выше, а сам путь является деталью реализации драйвера local, которая отсутствует у других драйверов томов.
Безопасный способ заглянуть внутрь — использовать одноразовый контейнер, который монтирует этот том:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /volЭтот метод работает с любым драйвером, учитывает те же права доступа, что видит основной контейнер, и не оставляет после себя никаких следов благодаря --rm.
Резервное копирование различных типов данных
Bind mount — это обычный каталог, поэтому любой инструмент резервного копирования на уровне файлов работает с ним без ограничений. Укажите путь на хосте в настройках резервного копирования, и задача будет выполнена. Для именованного тома (named volume) требуется дополнительный шаг, так как инструменту нужно получить доступ к его содержимому. Подключите том и каталог хоста к одному временному контейнеру, а затем создайте архив:
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .Восстановление выполняется обратным действием в новый том:
docker volume create myapp_pgdata_restored
docker run --rm \
-v myapp_pgdata_restored:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /datatar сохраняет числовые идентификаторы владельцев при запуске от имени root внутри контейнера, что позволяет приложению использовать восстановленный том.
Одно предостережение касается обоих типов данных. Копирование файлов базы данных во время её работы создает архив «движущейся цели», который при восстановлении может оказаться поврежденным. Сначала остановите сервис или выполните дамп с помощью штатной утилиты базы данных, как показано в docker compose exec -T db pg_dump -U postgres appdb > appdb.sql. Это создаст обычный файл, который можно включить в стандартную процедуру зашифрованного резервного копирования restic вместе с файлами compose.
Миграция bind mount в именованный том
Перенос выполняется путем копирования, а не переименования, и занимает около минуты.
docker compose down
docker volume create myapp_pgdata
docker run --rm \
-v "$PWD/data":/from \
-v myapp_pgdata:/to \
alpine sh -c 'cp -a /from/. /to/'cp -a сохраняет владельца, права доступа и временные метки, поэтому пользователь контейнера, который имел доступ к старой директории, сможет работать и с новым томом. Затем измените конфигурацию сервиса для использования pgdata:/var/lib/postgresql/data, добавьте pgdata в блок volumes: верхнего уровня, выполните docker compose up -d и изучите логи приложения перед удалением старой директории. Обратный процесс выполняется той же командой с заменой мест /from и /to.
Учитывайте один нюанс при тестировании. docker compose down не затрагивает именованные тома, но docker compose down -v удаляет все именованные тома, объявленные в проекте, и это действие невозможно отменить. Bind mount сохраняется в обоих случаях, так как Docker не является владельцем этой директории. Если вы еще не знакомы с командами жизненного цикла, руководство по основам Docker Compose для VPS поможет вам разобраться.
Выбор способа хранения для каждого сервиса
Определите, кто именно записывает данные в файл. Конфигурацию, которую вы редактируете в текстовом редакторе и фиксируете в git, следует размещать в bind mount, примонтированном в :ro, так как вам нужен доступ к файлам и версионирование. Состояние приложения, которое вы никогда не правите вручную, должно находиться в named volume, поскольку Docker корректно задает права доступа, а данные не зависят от пути на хосте.
Промежуточный случай — медиафайлы. Библиотека фотографий записывается приложением, но управляется вами, и часто она настолько велика, что требует отдельного диска. Используйте для неё bind mount к пути на этом диске и один раз принудительно установите владельца файлов. Это шаблон, к которому приходит большинство self-hosted стеков: named volumes для баз данных и кэшей, bind mounts для конфигураций и больших директорий, которые вы хотите контролировать. Сервис поддержки, такой как Chatwoot на VPS, использует именно этот подход: Postgres работает в named volume, а загруженные вложения хранятся по пути, для которого легко настроить резервное копирование.
FAQ
В чем разница между bind mount и именованным томом?
Bind mount отображает путь на хосте внутрь контейнера, поэтому обе стороны видят одну и ту же директорию, и вы можете редактировать её обычными инструментами. Именованный том — это хранилище, которое Docker создает и обслуживает самостоятельно; оно вызывается по имени и объявляется в блоке верхнего уровня volumes:. Практическое различие заключается в принадлежности: используйте bind mount для конфигурации, которую вы поддерживаете, и именованные тома для данных, которые обслуживает само приложение.
Почему я получаю ошибку "permission denied" при использовании bind mount, но не с именованным томом?
Пустой именованный том инициализируется на основе образа, поэтому он наследует права доступа, заданные в образе, и пользователь контейнера может записывать в него данные. Bind mount отображает директорию хоста в исходном виде, и если Docker пришлось создавать эту директорию, он назначил владельцем root. Выполните docker compose exec <service> id, чтобы увидеть числовой идентификатор пользователя, используемый контейнером, затем выполните sudo chown -R <uid>:<gid> для директории на хосте или укажите user: "1000:1000" в настройках сервиса.
Где Docker хранит именованные тома на диске?
При использовании драйвера по умолчанию local они находятся в /var/lib/docker/volumes/<volume>/_data, а команда docker volume inspect <volume> выводит точный путь Mountpoint. Читайте содержимое, если нужно что-то проверить, но записывайте данные только через контейнер. Редактирование файлов от имени root на хосте меняет владельца, что может привести к непредсказуемому поведению контейнера.
Как создать резервную копию именованного тома?
Запустите кратковременный контейнер, подключив к нему том и директорию хоста, а затем выполните архивацию из одного места в другое с помощью docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data .. Для баз данных используйте встроенные инструменты дампа вместо копирования файлов «на лету», так как копирование файлов во время активной записи может привести к восстановлению поврежденной базы.
Удаляет ли команда docker compose down мои тома?
docker compose down удаляет контейнеры и сети, но оставляет именованные тома на месте. docker compose down -v также удаляет все именованные тома, объявленные в проекте, без возможности восстановления. Bind mount не удаляются ни одной из этих команд, так как эта директория принадлежит хосту, а не Docker.