
Введение
Контейнеры в Docker изначально не предназначены для постоянного хранения данных. Если удалить контейнер, всё, что было записано внутрь него, исчезнет вместе с ним. Поэтому для баз данных, пользовательских файлов и других важных данных используют отдельные механизмы хранения.
В этой статье разберём, какие способы хранения данных есть в Docker, чем они отличаются, и в каких случаях стоит использовать каждый из них.
1. Типы хранилищ
Тип |
Кем управляется |
Где живёт |
Когда использовать |
|---|---|---|---|
Том (volume) |
Docker |
|
Продакшен, БД, всё, что нельзя терять |
Подключение хост-каталога (bind mount) |
Вы (хост) |
Любая папка на хосте |
Разработка, конфиги, сокеты (ну или другие специфические задачи в зависимости от проекта) |
Временная ФС в памяти (tmpfs) |
Ядро (RAM) |
Оперативная память |
Секреты, кэш, временные файлы |
Есть ещё именованный канал (named pipe) он же windows-механизм для связки хост-контейнер, например, для доступа к Docker Engine API изнутри контейнера. Не используется как способ хранения данных.
2. Тома (Volumes)
Тома — это хранилища данных, которыми управляет Docker. На Linux лежат в /var/lib/docker/volumes/<имя>/_data, но трогать их напрямую не стоит — Docker не гарантирует, что в этом случае всё продолжит нормально работать — это непредусмотренный и неподдерживаемый сценарий. Тома часто используют для БД из-за производительности — они лучше подходят для большого числа операций записи, чем слой для записи (writable layer) контейнера. Также том не увеличивает размер контейнера, все данные, записанные в том, не попадают в writable layer и не учитываются в docker ps --size. docker system df покажет тома отдельной строкой. Тома работают на Linux и Windows контейнерах.
2.1 Жизненный цикл тома
Событие |
Что происходит |
|---|---|
Контейнер удалён/удаляется |
Том остаётся |
Контейнер пересоздан |
Видит те же данные |
Том пустой, в образе уже есть файлы |
Docker копирует файлы из образа в том |
Том не пустой |
Контейнер видит данные тома (при этом содержимое образа скрыто) |
Удалить том можно командой
docker volume rm(docker volume rm [OPTIONS] VOLUME [VOLUME…]).
Основные команды для работы с томами:
Команда |
Что делает |
|---|---|
|
Создать том |
|
Список томов |
|
Информация о томе |
|
Удалить том (не получится, если используется хоть одним контейнером, даже остановленным) |
|
Удалить все неиспользуемые тома |
|
Сводка по месту на диске (тома, образы, слои сразу) |
2.2 Вопросы
Откуда берётся автосоздание тома?
Разберу на примере с postgres. Помните строку из таблицы выше: том пустой, в образе уже есть файлы, значит, Docker копирует их в том? Именно это поведение сработает, если запустить образ postgres без явного тома. Контейнер всё равно создаст себе анонимный том сам, и данные базы окажутся именно там, а не в слое для записи. Почему так происходит:
Механизм |
Что делает |
|---|---|
|
Объявляет точку монтирования; если вы сами не подключите туда том или подключение хост-каталога, Docker создаст анонимный том автоматически. Отсюда и поведение «из коробки» у postgres |
|
Отключает автокопирование содержимого образа в пустой том, если оно вам не нужно. Доступен как в |
Автокопирование данных |
Если созданный том пустой, а в соответствующем каталоге образа уже есть файлы, Docker один раз скопирует их в том. |
Как подключить том: --mount или -v?
Подключить готовый или новый том к контейнеру можно короткой записью -v или более явной --mount type=volume. Вроде одно и то же, но всё же есть различия:
|
|
|
|---|---|---|
Простота записи |
Короче |
Многословнее, но явнее (тип, имя, путь видны сразу) |
Опции драйвера тома |
❌ |
✅ |
|
❌ |
✅ |
Подключение к сервису в режиме кластера (Swarm) |
❌ |
✅ |
Как посмотреть, что реально подключено к контейнеру?
Если контейнер уже запущен, а вы не помните (или не знаете), какие тома, подключения хост-каталогов или tmpfs к нему подключены, не нужно лезть в compose.yaml или вспоминать команду запуска. Это можно посмотреть напрямую:
docker inspect <container>
Команда выводит большой JSON с полной информацией о контейнере. Среди прочего там есть массив Mounts. Именно в нём перечислены все монтирования. Если нужен только он, без остального JSON, можно отфильтровать вывод:
docker inspect <container> --format '{{json .Mounts}}'
В каждом элементе массива будут такие поля:
Поле |
Что показывает |
|---|---|
|
|
|
Путь на хосте (для подключения хост-каталога) или имя тома |
|
Путь внутри контейнера |
|
Доступен ли на запись ( |
ОС |
Тома (Volumes) против подключения хост-каталогов (Bind mount) по скорости |
|---|---|
Linux |
Разницы нет (ну лично я не ощущал такого, могу ошибаться) |
Windows / Mac |
Тома быстрее: подключение хост-каталогов идёт через слой виртуализации |
Именованные и анонимные тома: в чём разница?
Когда происходит запуск контейнера с томом, у которого нет имени, Docker создаёт том сам и присваивает ему случайный ID. Такой том называется анонимным. Если имя задать явно (-v myvol:/app), том именованный. Разница ток что с томом будет, когда контейнер удалят.
Тома |
Кто задаёт имя |
Что будет после удаления контейнера |
|---|---|---|
Анонимный |
Docker |
Может быть удалён вместе с контейнером, если он был запущен с |
Именованный |
Вы |
Останется, и его можно использовать снова |
Если контейнер запущен без
--rm, а потом вы удалите его обычнымdocker rm <id>, анонимный том НЕ удалится. Он останется висеть на хосте мёртвым грузом и будет занимать место. Чтобы он удалился вместе с контейнером, нужно явно указатьdocker rm -v <id>. Если другой контейнер подключает тот же анонимный том через--volumes-from, определение тома просто копируется, и том переживёт удаление первого контейнера, который его создал.
2.3 Дополнительные параметры томов
Кроме базового подключения, у томов есть параметры, которые +/- могут регулярно выручать в конкретных ситуациях:
Опция |
Что делает |
|---|---|
|
Том доступен только для чтения: удобно, когда несколько контейнеров читают одни и те же данные, но менять их не должны |
|
Подключить не весь том, а только его подпапку, например, чтобы несколько сервисов писали логи каждый в свою подпапку общего тома |
|
Отключить автокопирование содержимого образа в пустой том. Доступен в |
Распространение монтирования (Bind propagation) у томов |
Всегда |
У volume-subpath (кстати, такое может попасться на собесе в качестве вопроса) есть проблема, что подпапка должна существовать в томе до монтирования. Если её не будет, то монтирование упадёт с ошибкой. Если не знаете, как это делать, то можете узнать у ИИ, либо в доке, либо понять на учебном примере:
docker volume create logs # Сначала создаём подпапки docker run --rm --mount src=logs,dst=/logs alpine mkdir -p /logs/app1 /logs/app2 # Теперь можно монтировать подпапку docker run -d --mount src=logs,dst=/var/log/app1,volume-subpath=app1 app1:latest
2.3.1 --volumes-from: общий доступ к томам между контейнерами
--volumes-from позволяет подключить к одному контейнеру все тома другого контейнера. Не нужно вручную перечислять их имена или пути, Docker сделает это автоматически.
Факт |
Детали |
|---|---|
Что копируется |
Определение тома (путь монтирования), не сами данные |
Том после удаления первого контейнера |
Остаётся, если к нему подключён хоть один контейнер через |
Анонимный том и |
Переживёт удаление создавшего его контейнера — определение скопировано |
2.4 Тома в режиме кластера (Swarm)
Если вы запускаете не одиночный контейнер, а сервис в Swarm с томами, появляется нюанс, который можно не заметить и который ломает ожидания, что у сервиса «один общий том на всех»:
Факт |
Детали |
|---|---|
У каждой реплики сервиса |
Свой собственный локальный том с тем же именем (драйвер |
Общие данные между репликами |
Нужен общий драйвер (shared) NFS, S3 и т.п. (см. раздел 6) |
|
Тома при этом остаются, удалить их нужно отдельной командой |
3. Подключение хост-каталогов (Bind mount)
С bind mount я сталкивался в основном при разработке и в некоторых проектах. Смысл: вместо тома контейнеру дают доступ к папке или файлу, которые уже есть на хосте. Docker этим хранилищем не управляет. Контейнер просто работает с указанным путём.
Удобно это тем, что изменения сразу видны с обеих сторон. Поменял файл на хосте — контейнер сразу увидел изменения. Не нужно пересобирать образ только потому, что поправил пару строк в коде или изменил конфигурацию. Ну и bind mount не описывают в Dockerfile. Его задают при запуске контейнера через docker run (-v или --mount), либо в compose.yaml. Это всё, что нужно вкратце о нём знать.(В контексте безопасности ещё добавлю что bind mount даёт контейнеру права на запись в файловую систему хоста. Процесс внутри контейнера может создавать, изменять и удалять файлы на хосте, включая системные. Если контейнеру не нужна запись, используйте readonly / ro.)
Параметры подключения
Параметр |
Что делает |
|---|---|
|
Путь на хосте. В |
|
Путь внутри контейнера |
|
Только чтение |
|
Создать source-папку на хосте, если её нет (нужно указывать для |
Подключение поверх непустой папки в контейнере |
Старое содержимое скрывается на время работы контейнера |
Синтаксис и поведение bind mount
-v vs --mount для bind mount почти та же история, что и с томами, но отличия немного другие:
|
|
|
|---|---|---|
Создаёт папку автоматически |
✅ (всегда, как директорию) |
❌ (нужен |
SELinux-метки ( |
✅ |
❌ |
Bind propagation |
✅ (через опции типа |
✅ (через |
|
❌ |
✅ |
Читаемость |
Короче |
Явнее |
Для наглядности ниже ещё примеры, как одно и то же пишется через -v и через --mount, и отдельно пример с bind-create-src.
# -v: примонтировать текущую папку как readonly docker run -v $(pwd)/src:/app:ro nginx # --mount: то же самое(src может быть относительным) docker run --mount type=bind,src=$(pwd)/src,dst=/app,ro nginx # --mount с автосозданием папки docker run --mount type=bind,src=/home/user/mydir,dst=/app,bind-create-src nginx
Bind mount в Docker Compose
В Compose bind mount записывается через type: bind. Это явнее, чем строчная запись, и поддерживает все опции:
services: frontend: image: node:lts volumes: - type: bind source: ./static target: /opt/app/static read_only: true
Строчная запись (- ./static:/opt/app/static) тоже работает (менее явная).
Монтирование поверх непустой папки
Если в контейнере по указанному пути уже есть файлы, они скрываются на время работы контейнера (как USB-диск, примонтированный поверх /mnt). Убрать монтирование без пересоздания контейнера не получится.
bind-recursive
Если внутри примонтированной папки на хосте есть вложенные точки монтирования, они тоже попадают в контейнер по умолчанию. Работает только через --mount, флаг -v это не поддерживает. Только Linux, для значения readonly нужен kernel 5.12+:
Значение |
Что делает |
|---|---|
|
Вложенные маунты включены; readonly рекурсивно распространяется, если kernel 5.12+ |
|
Вложенные маунты не включаются в bind mount |
|
Вложенные маунты всегда read-write |
|
Вложенные маунты read-only (kernel 5.12+) |
3.1 Зачем нужно распространение монтирования (Bind propagation)?
Представим, что у нас на хосте внутри подключённой папки кто-то ещё примонтировал отдельное устройство или сетевую шару, т. е. появилась «вложенная» точка монтирования. Распространение монтирования как раз решает: увидит ли эту вложенную точку контейнер, и наоборот, увидят ли другие места на хосте то, что смонтировал внутри себя контейнер.
Звучит +/- абстрактно (в большинстве случаев я на практике это игнорирую), и настройка по умолчанию (rprivate, то есть «полная изоляция в обе стороны») — именно то, что нужно. Трогать стоит, только если осознанно нужно делить монтирование между несколькими контейнерами или хостом.
Значение |
Что делает |
|---|---|
|
Вложенные точки монтирования видны в обе стороны |
|
Видимость только в одну сторону: от оригинала к репликам |
|
Не передаётся никуда |
|
То же самое, но рекурсивно |
Работает только на Linux-хостах и не работает в Docker Desktop.
3.2 Проблемы подключения
Метки SELinux
Если на хосте включён SELinux, у файлов и папок есть метки безопасности, и без правильной метки контейнер может не получить доступ к подключённому каталогу, даже если обычные права на запись формально есть. Чтобы не настраивать SELinux вручную, у Docker есть возможность через опции проставить нужную метку.
Метка |
Когда использовать |
|---|---|
|
Содержимое общее для нескольких контейнеров |
|
Приватное содержимое для одного контейнера — осторожно с системными папками типа |
В сервисах Swarm |
|
Через |
Метки не настраиваются вообще, только через |
UID и GID
Здесь останавливаться не буду подробно, поэтому вкратце. Всё упирается в права. Docker сравнивает только UID и GID, если контейнер пишет файлы от root, то на хосте они тоже будут принадлежать root. Если UID не совпадают, можно получить ошибки записи или потом не суметь удалить файлы без sudo.
Поэтому обычно контейнер запускают с --user <uid>:<gid>. Подробнее я про UID и GID на Linux я писал в нескольких частях серий, рекомендую почитать их.
3.3 Ограничения
Если Docker работает на другом сервере, нельзя примонтировать файлы со своего компьютера. Docker увидит только файлы на том сервере.
Docker Desktop наверное будет исключением, поскольку daemon работает внутри Linux VM, но с Desktop прозрачно обрабатывает bind mounts с хостом — вы можете монтировать папки с вашего компьютера в контейнеры, как будто daemon запущен локально.
Контейнер зависит от конкретных папок хоста. Если запустить его на другой машине, где этих папок нет, он может не запуститься.
4. Временная файловая система в памяти (tmpfs)
Файловая система прямо в оперативной памяти. Про неё в основном стоит знать, что она работает только на Linux (на Windows прямого аналога нет; именованный канал там используется для других задач, для связи с Docker Engine API, и не является заменой tmpfs). Данные хранятся в памяти и удаляются после остановки контейнера. При использовании swap часть данных всё же может попасть на диск. Совместно между контейнерами её использовать нельзя, а перезагрузка в некоторых случаях может сбросить права доступа, поэтому обязательно задаём явно UID и GID.
Когда использовать?
Сценарий |
Почему tmpfs |
|---|---|
Временные файлы с высокой нагрузкой |
RAM быстрее диска |
Сессии, кэш |
Не нужны после рестарта контейнера |
Отдельный спорный момент: секреты и ключи шифрования. Я бы рекомендовал их не использовать из-за оговорки про файл подкачки выше, но есть сценарии, где это всё равно применяется.
Параметры
Параметр |
Доступен в |
По умолчанию |
Что делает |
|---|---|---|---|
|
|
50% RAM хоста |
Лимит размера файловой системы |
|
|
|
Права доступа в восьмеричном формате |
|
только |
— |
Доп. опции монтирования |
Сервисы в режиме кластера (Swarm) |
только |
— |
|
Совсем тонкие настройки |
контейнер с расширенными правами (privileged) + ручной |
— |
Риск для безопасности, только при реальной необходимости |
Полные опции --tmpfs
Флаг --tmpfs принимает опции через двоеточие: --tmpfs <путь>[:опции]
docker run --tmpfs /data:noexec,size=512m,mode=1777
Опция |
Что делает |
|---|---|
|
Лимит размера ( |
|
Права в восьмеричном формате ( |
|
Владелец ( |
|
Только чтение/чтение-запись (по умолчанию |
|
Запрет/разрешение запуска бинарников из tmpfs |
|
Отключить/включить setuid-биты |
|
Устройства нефункциональны/функциональны |
|
Лимит inode ( |
|
Лимит блоков ( |
|
Все операции ввода-вывода выполняются синхронно |
|
Все операции ввода-вывода выполняются асинхронно (по умолчанию) |
|
Обновления директорий выполняются синхронно |
|
Обновляет время доступа к файлу при каждом обращении |
|
Не обновляет время доступа к файлу |
|
Обновляет время доступа к директории при каждом обращении |
|
Не обновляет время доступа к директории |
Расширенные опции монтируются вручную через --privileged внутри контейнера (это открывает доступ к хосту, лучше не трогать без реальной необходимости). Ещё момент: если по указанному пути в контейнере уже есть файлы, они скроются на время работы, и вернуть их можно только пересоздав контейнер.
5. Тома в Docker Compose
В Docker Compose тома описываются прямо в compose.yaml. Работают они так же, как и при запуске через docker run: если тома ещё нет, Docker создаст его сам. Если том уже существует, будут использоваться сохранённые данные.
Например, в этом примере PostgreSQL хранит данные в одном томе, а n8n — в другом:
services: postgres: image: postgres:17-alpine restart: unless-stopped environment: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] start_period: 30s interval: 10s timeout: 5s retries: 5 n8n: image: n8nio/n8n:latest restart: unless-stopped environment: - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_PORT=5432 - DB_POSTGRESDB_DATABASE=${POSTGRES_DB} - DB_POSTGRESDB_USER=${POSTGRES_USER} - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD} - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY} - N8N_HOST=${N8N_HOST} - N8N_PORT=${N8N_PORT} - N8N_PROTOCOL=http - NODE_ENV=production - WEBHOOK_URL=https://${N8N_HOST}/ - GENERIC_TIMEZONE=${GENERIC_TIMEZONE} - N8N_SECURE_COOKIE=false volumes: - n8n_data:/home/node/.n8n depends_on: postgres: condition: service_healthy volumes: n8n_data: postgres_data:
После запуска Compose можно посмотреть состояние контейнеров и список созданных томов:
# запуск сервисов в фоне docker compose up -d # список контейнеров docker compose ps # список томов docker volume ls
Что нужно запомнить: docker compose down удаляет контейнеры и сеть проекта, но тома остаются. Если потом снова выполнить docker compose up -d, сервисы продолжат работать со старыми данными.
# остановить и удалить контейнеры docker compose down # остановить и удалить контейнеры вместе с томами docker compose down -v
Иногда используют внешние тома. Обычно это нужно, если том уже существует, и его не должен создавать Compose.
# создать том вручную docker volume create production-postgres-data # посмотреть информацию о томе docker volume inspect production-postgres-data
Чтобы Compose не пытался создать такой том сам, а просто использовал существующий, в compose.yaml его нужно явно пометить как внешний:
volumes: production-postgres-data: external: true
Без этой пометки Compose решит, что тома нет, и попробует создать новый под своим проектным именем — пример с одной только командой docker volume create без этого был бы неполным.
down vs down -v
Команда |
Контейнеры |
Сеть |
Тома |
|---|---|---|---|
|
удалены |
удалена |
остаются |
|
удалены |
удалена |
удалены |
6. Драйверы томов
6.1 Зачем это вообще нужно
По умолчанию том создаётся с драйвером local. Данные физически лежат на диске того же хоста, где запущен Docker. Но иногда нужно, чтобы несколько реплик сервиса или несколько хостов имели доступ к одним и тем же файлам, например, для отказоустойчивых приложений. Для этого можно либо добавить в приложение логику работы с облаком (S3 и т. п.), либо просто сменить драйвер тома (volume driver) без изменения кода приложения.
6.2 Варианты драйверов
Способ |
Где физически данные |
Когда нужен |
|---|---|---|
|
Диск этого же хоста |
Стандартный сценарий, без особых требований |
|
Сетевая шара |
Несколько хостов или реплик сервиса должны видеть одни и те же файлы |
Сторонний плагин (например, |
Удалённое хранилище по SFTP, S3 и т. п. |
Нужен протокол, которого нет из коробки |
|
Физический диск, раздел или loop-устройство |
Редкий, иногда даже продвинутый сценарий |
Если драйверу нужно передать опции, монтировать том в контейнер можно только через --mount, короткая запись -v их не поддерживает.
7. Драйверы хранения (storage drivers)
Не путать с драйверами томов из раздела выше. Драйвер хранения — это то, как Docker хранит слои образов (image layers) и слой записи (writable layer) контейнера на диске. Томов это не касается.
7.1 Зачем это знать
Любая запись внутри контейнера идёт через storage driver. Для нагрузок с интенсивной записью (базы данных и т. п.) лучше использовать тома и вообще не трогать writable layer, но знать, какой драйвер стоит, всё равно полезно.
docker info | grep "Storage Driver"
С Docker Engine 29.0+ дефолт для новых установок — containerd image store. Это не просто новый дефолт, а другая архитектура: containerd image store использует снапшоттеры (snapshotters), а не classic storage drivers (overlay2, btrfs и т. д.) Classic drivers при этом становятся legacy. Если вы обновились со старой версии, а не ставили Docker заново — у вас скорее всего остался classic driver, это нормально. Мигрировать на containerd image store можно отдельно, по инструкции из официальной документации.
7.2 Размер контейнеров на диске
Чтобы посмотреть, сколько реально занимает writable layer каждого запущенного контейнера:
docker ps --size
Вывод содержит два значения:
Поле |
Что показывает |
|---|---|
|
Размер writable layer конкретного контейнера |
|
|
Важно:
virtual sizeнельзя просто складывать по всем контейнерам. Контейнеры на одном образе делят его read-only слои — суммированиеvirtual sizeдаст завышенную цифру.
Что ещё занимает место, но не видно в docker ps --size:
Лог-файлы контейнера (если не настроена ротация — могут вырасти очень сильно).
Конфиги контейнеров в
/var/lib/docker/containers/.Данные в томах и bind mounts.
Память, записанная в swap.
7.3 Драйверы на Linux
Драйвер |
Когда используется |
Коротко |
|---|---|---|
|
Дефолт для classic mode |
Самый распространённый, работает везде, с него и стоит начинать |
|
Docker без root (rootless) на старых ядрах |
Не нужен, начиная с ядра 5.11 — с этой версии |
|
Хост на Btrfs, нужны снапшоты (snapshots) |
Требует отдельный Btrfs-раздел под |
|
Хост на ZFS, много контейнеров на одном хосте |
Много памяти, продвинутые возможности, без опыта с ZFS on Linux лучше не трогать |
|
Тестирование, среды без копирования при записи (copy-on-write) |
Полная копия каждого слоя — медленно и много места, только для отладки |
devicemapper— удалён, начиная с Docker Engine v25.0. На старых системах (CentOS/RHEL и других до массового перехода наoverlay2) может встречаться. Если видите его вdocker infoна сервере, то… тут по ситуации, но в основном это сигнал к миграции наoverlay2перед обновлением Docker :)
На Windows единственный вариант — windowsfilter (работает только с NTFS).
7.4 Какой драйвер на каком дистрибутиве
Дистрибутив |
Дефолт (classic) |
Альтернативы |
|---|---|---|
Ubuntu |
|
|
Debian |
|
|
CentOS / RHEL |
|
|
Fedora |
|
|
SLES 15 |
|
|
7.5 Файловая система под /var/lib/docker/
Драйвер |
Что подходит |
|---|---|
|
xfs (ftype=1), ext4, btrfs и другие |
|
любая |
|
только btrfs |
|
только zfs |
|
любая |
7.6 overlay2: как работает
Проще всего запомнить так: образ лежит в lowerdir, слой записи контейнера в upperdir, контейнер видит всё это через merged. Четвёртая директория — workdir — служебная, используется OverlayFS внутренне при атомарных операциях (например, при copy_up); в docker inspect не отображается, но присутствует на диске.
overlay2 работает на уровне файла, а не блока — в отличие от btrfs и zfs. Это значит, что copy_up копирует файл целиком, даже если изменился один байт. При работе с большими файлами это заметно влияет на производительность первой записи. Зато после copy_up все последующие записи в тот же файл идут напрямую в upperdir без накладных расходов.
overlay2 нативно поддерживает до 128 нижних слоёв (lowerdir), что улучшает производительность docker build и docker commit при работе с многослойными образами и снижает потребление inode.
Операция |
Что происходит |
|---|---|
Чтение файла (из образа) |
Читает из |
1-ая запись в файл |
|
|
Тоже триггерит |
Повторные записи в тот же файл |
Используется |
Удаление файла |
Создаётся whiteout-файл в |
Удаление директории |
Создаётся opaque directory в |
Ещё есть page cache sharing: несколько контейнеров читают один файл, а в памяти одна копия на всех. Удобно, когда контейнеров много.
Ограничения overlay2
rename(2) для директорий. Переименование директории работает только если и источник, и цель находятся в top layer (upperdir). Иначе возвращается ошибка EXDEV («cross-device link not permitted»). Приложения должны это обрабатывать — делать копию и удалять оригинал вместо переименования.
Проблема с open(2) и POSIX. Если приложение открывает файл сначала на чтение (O_RDONLY), а потом на запись (O_RDWR), после copy_up дескрипторы могут указывать на разные файлы: первый — на lowerdir, второй — на upperdir. Обходное решение: выполнить touch на файл перед работой с ним, чтобы copy_up произошёл заранее. Известная жертва этой проблемы — yum без пакета yum-plugin-ovl.
7.7 btrfs, zfs, vfs: отличия, и когда брать
btrfs и zfs работают на уровне блоков, а не файлов, в отличие от overlay2. На практике это лучше при интенсивной записи, но настройка сложнее.
|
|
|
|
|---|---|---|---|
Уровень работы |
Блочный |
Блочный |
Файловый (без CoW) |
Что нужно под |
Раздел с Btrfs |
ZFS-пул (zpool) |
Любая файловая система |
Снапшоты (snapshots) |
✅ |
✅ |
❌ |
Совместный кэш страниц (page cache sharing) |
❌ |
✅ (Single Copy ARC) |
❌ |
Потребление памяти |
Среднее |
Высокое |
Низкое |
Последовательная запись (sequential writes) |
Теряет до 50% из-за журналирования (journaling) |
Нормально |
Нормально |
Фрагментация |
Есть, нужен периодический |
Минимальная (блоки по 128k), но дефрагментация невозможна без переформатирования |
Нет |
Дедупликация (deduplication) |
Нет |
Есть, но лучше отключить — ест RAM (исключение: SAN/NAS/hardware RAID) |
Нет |
Хорошо подходит для |
Хостов, которые уже на Btrfs |
Много контейнеров на одном хосте (PaaS) |
Тестирования и отладки |
В прод |
Осторожно (известные проблемы) |
Только если есть опыт с ZFS on Linux |
Нет |
Дополнительно про btrfs
Если хост использует Btrfs, для Docker рекомендуется overlay2. Он проще в настройке, стабильнее и в некоторых случаев не требует отдельного раздела. При большом количестве мелких операций записи Btrfs может сообщить об ошибке out-of-space(хотя свободное место ещё есть, это связано с особенностями распределения chunks). Опция autodefrag помогает уменьшить фрагментацию файлов. На разных нагрузках эффект может отличаться, поэтому перед использованием на продакшене её лучше протестировать.
Если Docker хранит данные на SSD, можно включить оптимизации Btrfs для SSD.
Примеры
# Проверить состояние файловой системы btrfs filesystem show /var/lib/docker # Смонтировать с autodefrag mount -o autodefrag /dev/xvdf /var/lib/docker # Смонтировать с оптимизациями для SSD mount -o ssd /dev/xvdf /var/lib/docker
Дополнительно про zfs
В ZFS нет дефрагментации, поэтому, если пул сильно фрагментирован, его придётся пересоздать и восстановить из резервной копии. Дедупликацию без особой необходимости лучше не включать — она расходует много оперативной памяти. Размер writable layer контейнеров можно ограничить через storage-opts. Если используете ZFS на Linux, выбирайте нативную реализацию, а не FUSE, потому что она работает быстрее.
7.8 Смена драйвера
После смены все существующие образы и контейнеры станут недоступны, и Docker не сможет их прочитать через новый драйвер. Перед сменой сохраните всё нужное через docker save или docker push.
Откат иногда возможен. Если вы сменили драйвер и хотите вернуться к старому — просто верните прежнее значение в
daemon.jsonи перезапустите Docker. Образы и контейнеры, созданные под старым драйвером, снова станут доступны. Недоступны будут только те, что созданы под новым драйвером.
# Посмотреть текущий драйвер docker info | grep "Storage Driver" # Сменить — отредактировать /etc/docker/daemon.json { "storage-driver": "overlay2" } # Перезапустить Docker sudo systemctl restart docker
8. Бэкап и восстановление томов
Смысл: сам том никто не копирует. Я запускаю временный контейнер, подключаю к нему нужный том и через tar складываю его содержимое в архив. Если потом понадобится восстановление, делается всё то же самое, только архив распаковывается обратно.
8.1 Бэкап
docker run --rm -v my_volume:/data -v $(pwd):/backup \ alpine tar czf /backup/backup.tar.gz -C /data .
Запускает временный контейнер, подключает к нему том и папку на хосте, после чего архивирует содержимое тома в backup.tar.gz.
Том подключается как /data, а текущая папка на хосте — как /backup. В результате появляется архив backup.tar.gz.
Здесь стоит обратить внимание на -C /data .. Благодаря этому в архив попадёт содержимое тома, а не папка data целиком. Потом это упростит восстановление.
8.2 Восстановление
docker run --rm -v my_volume:/data -v $(pwd):/backup \ alpine sh -c "cd /data && tar xzf /backup/backup.tar.gz"
Запускает временный контейнер и выполняет обратную операцию: распаковывает архив обратно в том.
Архив распаковывается обратно в том, и файлы возвращаются на свои места.
Конечно, можно как-нибудь иначе сделать бэкап, но это у меня пока самый простой вариант.
Заключение
Если запомнить только одну мысль, то она простая: важные данные не должны храниться внутри контейнера. Для постоянного хранения обычно используют тома, для работы с файлами хоста — bind mount, а для временных данных — tmpfs.
Остальные темы, такие как драйверы, SELinux, Swarm и другие нюансы, пригодятся уже в более сложных сценариях. К ним всегда можно вернуться, когда возникнет конкретная задача.
© 2026 ООО «МТ ФИНАНС»