Введение

Контейнеры в Docker изначально не предназначены для постоянного хранения данных. Если удалить контейнер, всё, что было записано внутрь него, исчезнет вместе с ним. Поэтому для баз данных, пользовательских файлов и других важных данных используют отдельные механизмы хранения.

В этой статье разберём, какие способы хранения данных есть в Docker, чем они отличаются, и в каких случаях стоит использовать каждый из них.

1. Типы хранилищ

Тип

Кем управляется

Где живёт

Когда использовать

Том (volume)

Docker

/var/lib/docker/volumes/<имя>/_data

Продакшен, БД, всё, что нельзя терять

Подключение хост-каталога (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…]).

Основные команды для работы с томами:

Команда

Что делает

docker volume create

Создать том

docker volume ls

Список томов

docker volume inspect

Информация о томе

docker volume rm

Удалить том (не получится, если используется хоть одним контейнером, даже остановленным)

docker volume prune

Удалить все неиспользуемые тома

docker system df

Сводка по месту на диске (тома, образы, слои сразу)

2.2 Вопросы

Откуда берётся автосоздание тома?

Разберу на примере с postgres. Помните строку из таблицы выше: том пустой, в образе уже есть файлы, значит, Docker копирует их в том? Именно это поведение сработает, если запустить образ postgres без явного тома. Контейнер всё равно создаст себе анонимный том сам, и данные базы окажутся именно там, а не в слое для записи. Почему так происходит:

Механизм

Что делает

VOLUME /path в Dockerfile образа

Объявляет точку монтирования; если вы сами не подключите туда том или подключение хост-каталога, Docker создаст анонимный том автоматически. Отсюда и поведение «из коробки» у postgres

volume-nocopy

Отключает автокопирование содержимого образа в пустой том, если оно вам не нужно. Доступен как в --mount, так и в -v

Автокопирование данных

Если созданный том пустой, а в соответствующем каталоге образа уже есть файлы, Docker один раз скопирует их в том.

Как подключить том: --mount или -v?

Подключить готовый или новый том к контейнеру можно короткой записью -v или более явной --mount type=volume. Вроде одно и то же, но всё же есть различия:

-v

--mount

Простота записи

Короче

Многословнее, но явнее (тип, имя, путь видны сразу)

Опции драйвера тома

volume-subpath (подключить подпапку)

Подключение к сервису в режиме кластера (Swarm)

Как посмотреть, что реально подключено к контейнеру?

Если контейнер уже запущен, а вы не помните (или не знаете), какие тома, подключения хост-каталогов или tmpfs к нему подключены, не нужно лезть в compose.yaml или вспоминать команду запуска. Это можно посмотреть напрямую:

docker inspect <container>

Команда выводит большой JSON с полной информацией о контейнере. Среди прочего там есть массив Mounts. Именно в нём перечислены все монтирования. Если нужен только он, без остального JSON, можно отфильтровать вывод:

docker inspect <container> --format '{{json .Mounts}}'

В каждом элементе массива будут такие поля:

Поле

Что показывает

Type

volume / bind / tmpfs

Source / Name

Путь на хосте (для подключения хост-каталога) или имя тома

Destination

Путь внутри контейнера

RW

Доступен ли на запись (true/false)

ОС

Тома (Volumes) против подключения хост-каталогов (Bind mount) по скорости

Linux

Разницы нет (ну лично я не ощущал такого, могу ошибаться)

Windows / Mac

Тома быстрее: подключение хост-каталогов идёт через слой виртуализации

Именованные и анонимные тома: в чём разница?

Когда происходит запуск контейнера с томом, у которого нет имени, Docker создаёт том сам и присваивает ему случайный ID. Такой том называется анонимным. Если имя задать явно (-v myvol:/app), том именованный. Разница ток что с томом будет, когда контейнер удалят.

Тома

Кто задаёт имя

Что будет после удаления контейнера

Анонимный

Docker

Может быть удалён вместе с контейнером, если он был запущен с --rm

Именованный

Вы

Останется, и его можно использовать снова

Если контейнер запущен без --rm, а потом вы удалите его обычным docker rm <id>, анонимный том НЕ удалится. Он останется висеть на хосте мёртвым грузом и будет занимать место. Чтобы он удалился вместе с контейнером, нужно явно указать docker rm -v <id>. Если другой контейнер подключает тот же анонимный том через --volumes-from, определение тома просто копируется, и том переживёт удаление первого контейнера, который его создал.

2.3 Дополнительные параметры томов

Кроме базового подключения, у томов есть параметры, которые +/- могут регулярно выручать в конкретных ситуациях:

Опция

Что делает

ro / readonly

Том доступен только для чтения: удобно, когда несколько контейнеров читают одни и те же данные, но менять их не должны

volume-subpath

Подключить не весь том, а только его подпапку, например, чтобы несколько сервисов писали логи каждый в свою подпапку общего тома logs, не видя чужих

volume-nocopy

Отключить автокопирование содержимого образа в пустой том. Доступен в --mount и в -v

Распространение монтирования (Bind propagation) у томов

Всегда rprivate, не настраивается (в отличие от подключения хост-каталогов, см. раздел 3.1)

У 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 сделает это автоматически.

Факт

Детали

Что копируется

Определение тома (путь монтирования), не сами данные

Том после удаления первого контейнера

Остаётся, если к нему подключён хоть один контейнер через --volumes-from

Анонимный том и --volumes-from

Переживёт удаление создавшего его контейнера — определение скопировано

2.4 Тома в режиме кластера (Swarm)

Если вы запускаете не одиночный контейнер, а сервис в Swarm с томами, появляется нюанс, который можно не заметить и который ломает ожидания, что у сервиса «один общий том на всех»:

Факт

Детали

У каждой реплики сервиса

Свой собственный локальный том с тем же именем (драйвер local)

Общие данные между репликами

Нужен общий драйвер (shared) NFS, S3 и т.п. (см. раздел 6)

docker service rm

Тома при этом остаются, удалить их нужно отдельной командой

3. Подключение хост-каталогов (Bind mount)

С bind mount я сталкивался в основном при разработке и в некоторых проектах. Смысл: вместо тома контейнеру дают доступ к папке или файлу, которые уже есть на хосте. Docker этим хранилищем не управляет. Контейнер просто работает с указанным путём.

Удобно это тем, что изменения сразу видны с обеих сторон. Поменял файл на хосте — контейнер сразу увидел изменения. Не нужно пересобирать образ только потому, что поправил пару строк в коде или изменил конфигурацию. Ну и bind mount не описывают в Dockerfile. Его задают при запуске контейнера через docker run (-v или --mount), либо в compose.yaml. Это всё, что нужно вкратце о нём знать.(В контексте безопасности ещё добавлю что bind mount даёт контейнеру права на запись в файловую систему хоста. Процесс внутри контейнера может создавать, изменять и удалять файлы на хосте, включая системные. Если контейнеру не нужна запись, используйте readonly / ro.)

Параметры подключения

Параметр

Что делает

source / src

Путь на хосте. В --mount принимает как абсолютный, так и относительный путь. В -v надёжнее использовать $(pwd)/... или полный путь

destination / target

Путь внутри контейнера

readonly / ro

Только чтение

bind-create-src

Создать source-папку на хосте, если её нет (нужно указывать для --mount; -v создаёт её сама)

Подключение поверх непустой папки в контейнере

Старое содержимое скрывается на время работы контейнера

Синтаксис и поведение bind mount

-v vs --mount для bind mount почти та же история, что и с томами, но отличия немного другие:

-v

--mount

Создаёт папку автоматически

✅ (всегда, как директорию)

❌ (нужен bind-create-src)

SELinux-метки (:z, :Z)

Bind propagation

✅ (через опции типа rshared)

✅ (через bind-propagation=)

bind-recursive

Читаемость

Короче

Явнее

Для наглядности ниже ещё примеры, как одно и то же пишется через -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+:

Значение

Что делает

enabled (по умолчанию)

Вложенные маунты включены; readonly рекурсивно распространяется, если kernel 5.12+

disabled

Вложенные маунты не включаются в bind mount

writable

Вложенные маунты всегда read-write

readonly

Вложенные маунты read-only (kernel 5.12+)

3.1 Зачем нужно распространение монтирования (Bind propagation)?

Представим, что у нас на хосте внутри подключённой папки кто-то ещё примонтировал отдельное устройство или сетевую шару, т. е. появилась «вложенная» точка монтирования. Распространение монтирования как раз решает: увидит ли эту вложенную точку контейнер, и наоборот, увидят ли другие места на хосте то, что смонтировал внутри себя контейнер.

Звучит +/- абстрактно (в большинстве случаев я на практике это игнорирую), и настройка по умолчанию (rprivate, то есть «полная изоляция в обе стороны») — именно то, что нужно. Трогать стоит, только если осознанно нужно делить монтирование между несколькими контейнерами или хостом.

Значение

Что делает

shared

Вложенные точки монтирования видны в обе стороны

slave

Видимость только в одну сторону: от оригинала к репликам

private

Не передаётся никуда

rshared / rslave / rprivate (по умолчанию)

То же самое, но рекурсивно

Работает только на Linux-хостах и не работает в Docker Desktop.

3.2 Проблемы подключения

Метки SELinux

Если на хосте включён SELinux, у файлов и папок есть метки безопасности, и без правильной метки контейнер может не получить доступ к подключённому каталогу, даже если обычные права на запись формально есть. Чтобы не настраивать SELinux вручную, у Docker есть возможность через опции проставить нужную метку.

Метка

Когда использовать

z

Содержимое общее для нескольких контейнеров

Z

Приватное содержимое для одного контейнера — осторожно с системными папками типа /home или /usr

В сервисах Swarm

:z / :Z и :ro игнорируются

Через --mount

Метки не настраиваются вообще, только через -v

UID и GID

Здесь останавливаться не буду подробно, поэтому вкратце. Всё упирается в права. Docker сравнивает только UID и GID, если контейнер пишет файлы от root, то на хосте они тоже будут принадлежать root. Если UID не совпадают, можно получить ошибки записи или потом не суметь удалить файлы без sudo.

Поэтому обычно контейнер запускают с --user <uid>:<gid>. Подробнее я про UID и GID на Linux я писал в нескольких частях серий, рекомендую почитать их.

3.3 Ограничения

  1. Если Docker работает на другом сервере, нельзя примонтировать файлы со своего компьютера. Docker увидит только файлы на том сервере.

Docker Desktop наверное будет исключением, поскольку daemon работает внутри Linux VM, но с Desktop прозрачно обрабатывает bind mounts с хостом — вы можете монтировать папки с вашего компьютера в контейнеры, как будто daemon запущен локально.

  1. Контейнер зависит от конкретных папок хоста. Если запустить его на другой машине, где этих папок нет, он может не запуститься.

4. Временная файловая система в памяти (tmpfs)

Файловая система прямо в оперативной памяти. Про неё в основном стоит знать, что она работает только на Linux (на Windows прямого аналога нет; именованный канал там используется для других задач, для связи с Docker Engine API, и не является заменой tmpfs). Данные хранятся в памяти и удаляются после остановки контейнера. При использовании swap часть данных всё же может попасть на диск. Совместно между контейнерами её использовать нельзя, а перезагрузка в некоторых случаях может сбросить права доступа, поэтому обязательно задаём явно UID и GID.

Когда использовать?

Сценарий

Почему tmpfs

Временные файлы с высокой нагрузкой

RAM быстрее диска

Сессии, кэш

Не нужны после рестарта контейнера

Отдельный спорный момент: секреты и ключи шифрования. Я бы рекомендовал их не использовать из-за оговорки про файл подкачки выше, но есть сценарии, где это всё равно применяется.

Параметры

Параметр

Доступен в

По умолчанию

Что делает

tmpfs-size

--mount и --tmpfs

50% RAM хоста

Лимит размера файловой системы

tmpfs-mode

--mount и --tmpfs

1777

Права доступа в восьмеричном формате

ro/rw, noexec, nosuid, uid/gid и др.

только --tmpfs

Доп. опции монтирования

Сервисы в режиме кластера (Swarm)

только --mount

--tmpfs не поддерживается

Совсем тонкие настройки

контейнер с расширенными правами (privileged) + ручной mount

Риск для безопасности, только при реальной необходимости

Полные опции --tmpfs

Флаг --tmpfs принимает опции через двоеточие: --tmpfs <путь>[:опции]

docker run --tmpfs /data:noexec,size=512m,mode=1777

Опция

Что делает

size

Лимит размера (size=64m, size=1g)

mode

Права в восьмеричном формате (mode=1777)

uid/gid

Владелец (uid=1000,gid=1000). Стоит задавать явно, иначе после рестарта права могут сброситься

ro/rw

Только чтение/чтение-запись (по умолчанию rw)

noexec/exec

Запрет/разрешение запуска бинарников из tmpfs

nosuid/suid

Отключить/включить setuid-биты

nodev/dev

Устройства нефункциональны/функциональны

nr_inodes

Лимит inode (nr_inodes=400k)

nr_blocks

Лимит блоков (nr_blocks=1024)

sync

Все операции ввода-вывода выполняются синхронно

async

Все операции ввода-вывода выполняются асинхронно (по умолчанию)

dirsync

Обновления директорий выполняются синхронно

atime

Обновляет время доступа к файлу при каждом обращении

noatime

Не обновляет время доступа к файлу

diratime

Обновляет время доступа к директории при каждом обращении

nodiratime

Не обновляет время доступа к директории

Расширенные опции монтируются вручную через --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

Команда

Контейнеры

Сеть

Тома

docker compose down

удалены

удалена

остаются

docker compose down -v

удалены

удалена

удалены

6. Драйверы томов

6.1 Зачем это вообще нужно

По умолчанию том создаётся с драйвером local. Данные физически лежат на диске того же хоста, где запущен Docker. Но иногда нужно, чтобы несколько реплик сервиса или несколько хостов имели доступ к одним и тем же файлам, например, для отказоустойчивых приложений. Для этого можно либо добавить в приложение логику работы с облаком (S3 и т. п.), либо просто сменить драйвер тома (volume driver) без изменения кода приложения.

6.2 Варианты драйверов

Способ

Где физически данные

Когда нужен

local (по умолчанию)

Диск этого же хоста

Стандартный сценарий, без особых требований

local + NFS/CIFS

Сетевая шара

Несколько хостов или реплик сервиса должны видеть одни и те же файлы

Сторонний плагин (например, rclone)

Удалённое хранилище по SFTP, S3 и т. п.

Нужен протокол, которого нет из коробки

local + блочное устройство

Физический диск, раздел или 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

Вывод содержит два значения:

Поле

Что показывает

size

Размер writable layer конкретного контейнера

virtual size

size + read-only слои образа

Важно: virtual size нельзя просто складывать по всем контейнерам. Контейнеры на одном образе делят его read-only слои — суммирование virtual size даст завышенную цифру.

Что ещё занимает место, но не видно в docker ps --size:

  • Лог-файлы контейнера (если не настроена ротация — могут вырасти очень сильно).

  • Конфиги контейнеров в /var/lib/docker/containers/.

  • Данные в томах и bind mounts.

  • Память, записанная в swap.

7.3 Драйверы на Linux

Драйвер

Когда используется

Коротко

overlay2

Дефолт для classic mode

Самый распространённый, работает везде, с него и стоит начинать

fuse-overlayfs

Docker без root (rootless) на старых ядрах

Не нужен, начиная с ядра 5.11 — с этой версии overlay2 сам работает в rootless mode

btrfs

Хост на Btrfs, нужны снапшоты (snapshots)

Требует отдельный Btrfs-раздел под /var/lib/docker/; официально только Ubuntu, Debian, SLES

zfs

Хост на ZFS, много контейнеров на одном хосте

Много памяти, продвинутые возможности, без опыта с ZFS on Linux лучше не трогать

vfs

Тестирование, среды без копирования при записи (copy-on-write)

Полная копия каждого слоя — медленно и много места, только для отладки

devicemapper — удалён, начиная с Docker Engine v25.0. На старых системах (CentOS/RHEL и других до массового перехода на overlay2) может встречаться. Если видите его в docker info на сервере, то… тут по ситуации, но в основном это сигнал к миграции на overlay2 перед обновлением Docker :)

На Windows единственный вариант — windowsfilter (работает только с NTFS).

7.4 Какой драйвер на каком дистрибутиве

Дистрибутив

Дефолт (classic)

Альтернативы

Ubuntu

overlay2

zfs, vfs

Debian

overlay2

vfs

CentOS / RHEL

overlay2

zfs, vfs

Fedora

overlay2

zfs, vfs

SLES 15

overlay2

vfs

7.5 Файловая система под /var/lib/docker/

Драйвер

Что подходит

overlay2

xfs (ftype=1), ext4, btrfs и другие

fuse-overlayfs

любая

btrfs

только btrfs

zfs

только zfs

vfs

любая

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.

Операция

Что происходит

Чтение файла (из образа)

Читает из lowerdir

1-ая запись в файл

copy_up: файл целиком копируется в upperdir и там меняется (даже если поменялась одна строка)

chmod / chown на файл из образа

Тоже триггерит copy_up — файл целиком копируется в upperdir, даже если изменились только метаданные

Повторные записи в тот же файл

Используется upperdir

Удаление файла

Создаётся whiteout-файл в upperdir, скрывающий оригинал из lowerdir

Удаление директории

Создаётся opaque directory в upperdir — скрывает директорию из lowerdir, хотя та там остаётся

Ещё есть 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. На практике это лучше при интенсивной записи, но настройка сложнее.

btrfs

zfs

vfs

Уровень работы

Блочный

Блочный

Файловый (без CoW)

Что нужно под /var/lib/docker/

Раздел с Btrfs

ZFS-пул (zpool)

Любая файловая система

Снапшоты (snapshots)

Совместный кэш страниц (page cache sharing)

✅ (Single Copy ARC)

Потребление памяти

Среднее

Высокое

Низкое

Последовательная запись (sequential writes)

Теряет до 50% из-за журналирования (journaling)

Нормально

Нормально

Фрагментация

Есть, нужен периодический balance по крону

Минимальная (блоки по 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 ООО «МТ ФИНАНС»

Комментарии (0)