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
Это самое большое практическое различие, и оно в основном сводится к одному правилу: именованный том, который пуст при первом использовании, заполняется данными из образа, а bind mount — никогда.
Когда Docker монтирует пустой именованный том поверх директории, которая уже содержит данные в образе, он копирует эти данные в том, сохраняя владельца и права доступа, установленные в образе. Официальный образ Postgres поставляется с /var/lib/postgresql/data, принадлежащей его собственному пользователю postgres, поэтому том оказывается в собственности того же числового идентификатора, и база данных запускается.
Это начальное заполнение — единственный случай, когда именованный том получает преимущество при старте. Docker управляет тем, где находится том, но контейнеры читают и записывают его с помощью обычных операций файловой системы, поэтому права владения и режимы доступа применяются так же, как и при bind mount. Именованный том, смонтированный по пути, которого нет в образе, или том, впервые заполненный другим образом или пользователем, не получает прав владения от образа, и процесс без прав root также может получить там отказ. Проверка id, описанная ниже, работает для обоих типов.
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 с обеих сторон, если только daemon не запущен с переназначением пространств имен пользователей (userns-remap, настраивается в /etc/docker/daemon.json или через аргументы командной строки dockerd). Переназначение сдвигает каждый идентификатор контейнера в подчиненный диапазон на хосте. В примере Docker, где диапазон начинается с 231072, uid 0 контейнера — это uid 231072 на хосте, поэтому uid 1000 контейнера — это uid 232072 на хосте.
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: не может закрепить контейнер за вашим числом на хосте, так как заданный вами идентификатор также сдвигается: user: "1000:1000" все равно попадет на uid 232072 на хосте в этом примере диапазона. Вместо этого измените владельца (chown) директории на хосте на соответствующий отображенный идентификатор (ваш диапазон находится в /etc/subuid и /etc/subgid). В документации Docker в качестве способа отключения переназначения для отдельного контейнера упоминаются только --userns=host в docker container create, run или exec. Закрепление user: удобнее для приложения, которое вы написали сами. Изменение владельца директории на хосте безопаснее для образа, который писали не вы, так как некоторые образы запускают 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 стеков: именованные томы для баз данных и кэшей, bind mounts для конфигураций и больших директорий, которые вы хотите контролировать. Сервис поддержки, такой как Chatwoot на VPS, использует именно этот подход: Postgres работает в именованном томе, а загруженные вложения хранятся по пути, для которого можно настроить резервное копирование.
FAQ
В чем разница между bind mount и именованным томом?
Bind mount отображает путь на хосте внутрь контейнера, поэтому обе стороны видят одну и ту же директорию, и вы можете редактировать её обычными инструментами. Именованный том — это хранилище, которое Docker создает и контролирует; оно упоминается по имени и объявляется в блоке верхнего уровня volumes:. Практическое разделение заключается в правах владения: bind mounts — для конфигурации, которую вы поддерживаете, именованные томы — для данных, которые поддерживает приложение.
Почему я получаю «permission denied» при использовании bind mount, но не с именованным томом?
Пустой именованный том заполняется данными из образа, поэтому он обычно наследует права владения, установленные образом, и пользователь контейнера может записывать в него данные. Он все равно применяет ограничения прав, поэтому может выдать ту же ошибку, если в образе по этому пути ничего нет. Bind mount показывает директорию на хосте в точности такой, какая она есть, и если Docker пришлось создавать эту директорию, он сделал её владельцем root. Запустите docker compose exec <service> id, чтобы увидеть числовой идентификатор, который использует контейнер, затем выполните sudo chown -R <uid>:<gid> для директории на хосте или, если daemon не переназначает пространства имен пользователей, установите user: "1000:1000" в описании сервиса. Если daemon использует переназначение пространств имен пользователей, измените владельца (chown) на отображенный идентификатор хоста, так как идентификаторы контейнера сдвинуты на хосте, а user: этого не меняет.
Где 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 mounts не удаляются ни одной из этих команд, так как эта директория принадлежит хосту, а не Docker.