Docker Compose: bind mount или именованный том
Разберите bind mount и именованный том в Docker Compose: что выбрать для конфигурации и данных, как избежать ошибок прав и проверить, сделать резервную копию и перенести тома.
Bind mount или именованный том: краткий ответ
В Docker Compose есть два вида томов. Выбор зависит от того, кто владеет файлами. Используйте bind mount для файлов, которые вы записываете и читаете сами: конфигурации, шаблонов и статических сайтов. Используйте именованный том для данных, которыми владеет приложение: файлов базы данных, поисковых индексов и загруженных медиафайлов. Bind mount указывает на путь на хосте, который можно открыть в редакторе. Именованный том — это хранилище, которое Docker создает и отслеживает самостоятельно. Доступ к нему осуществляется через Docker.
Оба варианта указываются в одном ключе volumes: внутри сервиса, поэтому их часто путают. Различие определяется левой частью перед двоеточием. Если левая часть начинается с . или /, это путь на хосте, то есть bind mount. Любое другое значение является именем, то есть именованным томом. Это имя также должно быть объявлено в блоке верхнего уровня 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.
Запустите Compose и просмотрите список созданных 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 mount
Это главное практическое различие. Оно связано с одним правилом: при первом использовании пустой именованный том заполняется из образа, а bind mount — никогда.
Когда Docker монтирует пустой именованный том поверх каталога, в котором уже есть данные из образа, он копирует эти данные в том вместе с владельцем и режимами доступа, заданными в образе. В официальном образе Postgres /var/lib/postgresql/data принадлежит пользователю postgres. Поэтому том получает тот же числовой идентификатор владельца, и база данных запускается.
Bind mount работает иначе. Контейнер видит содержимое хоста, включая владельца, а содержимое образа по этому пути скрывается. Если каталога на хосте нет, демон Docker создает его и запускается от имени root, поэтому каталог принадлежит root:root. Процесс контейнера, работающий от имени непривилегированного пользователя, не может записать в него:
PermissionError: [Errno 13] Permission denied: '/data/app.db'Исправление состоит в согласовании числовых идентификаторов. Владельцы в bind mount сравниваются по числовому идентификатору пользователя, а не по имени, потому что у контейнера собственный /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: обычно проще. Для образа, который вы не разрабатывали, безопаснее изменить владельца каталога на хосте, поскольку некоторые образы запускают entrypoint от имени root, затем сбрасывают привилегии и ожидают определенного владельца вложенных файлов и каталогов.
Также следует учитывать еще две особенности. В Fedora, RHEL и других системах с включенным SELinux (security-enhanced Linux) bind mount блокируется, пока для него не будет изменена метка безопасности. Поэтому для пути, совместно используемого контейнерами, добавьте :z, а для пути, предназначенного только для одного контейнера, — :Z, записав параметр как - ./data:/data:Z. Кроме того, bind mount отдельного файла, а не каталога, перестает работать, если редактор заменяет файл вместо записи в него, поскольку 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/dockerdocker 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 — это обычный каталог, поэтому любой инструмент для резервного копирования на уровне файлов уже умеет с ним работать. Укажите в резервной копии путь на хосте — и всё готово. Для именованного тома требуется дополнительный шаг, поскольку инструмент должен получить к нему доступ. Подключите том и каталог на хосте к одному временно запускаемому контейнеру, затем создайте архив:
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. Так файл будет доступен для просмотра и версионирования. Состояние приложения, которое вы никогда не открываете вручную, должно находиться в именованном томе. Docker правильно задаст права доступа, а данные не будут зависеть от пути на хосте.
Смешанный случай — медиаданные. Приложение записывает фототеку, но управлять ею также будете вы. Кроме того, она часто занимает достаточно места, чтобы выделить для нее отдельный диск. Смонтируйте ее в bind mount по пути на этом диске и один раз явно задайте владельца. Обычно именно так организуют self-hosted стеки: именованные тома используют для баз данных и кэшей, а bind mount — для конфигурации и больших каталогов, которые вам нужны.
FAQ
В чем разница между bind mount и именованным томом?
Bind mount подключает путь на хосте к контейнеру. Обе стороны видят один и тот же каталог, и вы можете изменять его обычными инструментами. Именованный том создает и обслуживает Docker. На него ссылаются по имени и объявляют в блоке верхнего уровня volumes:. Практическое различие связано с владельцем данных: используйте bind mount для конфигурации, которую обслуживаете вы, а именованные тома — для данных, которые обслуживает приложение.
Почему при использовании bind mount появляется ошибка «permission denied», а при использовании именованного тома — нет?
Пустой именованный том заполняется данными из образа. Поэтому он наследует владельца, заданного в образе, и пользователь контейнера может записывать в него данные. 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.