skip to main content

Управление данными в Docker

js

Кратко 🔗

Если вы ничего не настраиваете специально, все данные приложения хранятся в контейнере Docker. После остановки контейнера все данные теряются. Но это не единственный способ. Можно использовать оперативную память и файловую систему компьютера, на котором установлен Docker Engine. Существует несколько типов хранилищ данных:
— тома (volumes);
— связанные папки, примонтированные к контейнеру как внешние диски (bind mounts);
— часть оперативной памяти для работы с данными (tmpfs mounts или npipe mounts).

Как понять 🔗

Не важно какой тип хранилища вы выберете, данные для приложения будут расположены в заданной вами папке внутри контейнера. Технология работает бесшовно, но имеет свои накладные расходы для каждого конкретного типа.

Типы управления данными:

Типы управления данными в Docker

Связанные папки (bind mounts) 🔗

Этот тип позволяет связать папку на компьютере пользователя (хосте, на котором установлен Docker Engine) и папку в контейнере. Работать в контейнере и на хосте с такой папкой можно одновременно. Все изменения будут отображаться и там, и там. Механизм bind mounts подразумевает, что данные могут быть изменены в любое время как из подключенного контейнера, так и непосредственно на хосте.

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

Когда используем 🔗

Используем связанные папки когда конфигурационные файлы на хосте и в контейнере одни и те же. Именно этот тип использует и Docker для автоматического монтирования конфигурации DNS хоста.

Используем связанные папки когда работаем с исходным кодом и артефактами сборок. Можно использовать системы сборки для исходного кода внутри контейнера. Вы меняете код, бандлер, который находится внутри контейнера, это видит, и код попадет в новую сборку. Другой вариант использования — работа с уже собранными бандлами, например, для тестирования или отладки приложений.

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

Как используем 🔗

Связанные папки появились в Docker с самых первых релизов. Это удобный инструмент, но у него есть определенные ограничения.

Чтобы связать папку на хосте с папкой внутри контейнера можно воспользоваться флагами -v или --mount. $(pwd) в командах ниже означает, что примонтируется текущая папка на хосте (компьютере пользователя).

Первый вариант (флаг -v):

docker run -d \
-it \
--name devtest \
-v "$(pwd)"/target:/app \
node:lts

Третье поле поддерживает следующие опции: rprivate, private, rshared, shared, rslave, slave, ro, z и Z.

Первая группа параметров позволяет управлять тем, как будут влиять изменения в одной точке монтирования тома, на другие точки монтирования. По умолчанию используется rprivate, что означает — никак.

Последние три параметра могут быть указаны только для флага -v. Значение ro определяет режим только для чтения. Папка на хосте не может быть изменена внутри контейнера. Значение z обозначает, что папка на хосте может быть использована несколькими контейнерами. Значение Z обозначает, что папка используется только одним контейнером. Не указывайте значение Z для системных папок, например, /usr или /home. Это приведет к тому, что работа операционной системы на хосте будет парализована. Будьте аккуратны!

Второй вариант (флаг --mount):

docker run -d \
-it \
--name devtest \
--mount type=bind,source="$(pwd)"/target,target=/app \
node:lts
Ключ `bind-propagation`

Для флага --mount есть ключ bind-propagation, который работает только на Linux (операционная система контейнера и хоста должна поддерживать этот режим работы).

Представьте, есть две точки монтирования /mnt1 и /mnt2, к которым привязана одна и та же папка на хосте. Значения ключа bind-propagation определяют, что будет происходить, если будут появляться подпапки в связанной папке. Что будет происходить с /mnt2/sub при монтировании /mnt1/sub? Возможны следующие варианты:

shared указывает на то, что изменения для точки монтирования /mnt1/sub будут в точности отражаться в /mnt2/sub и наоборот;
slave указывает на то же, что shared, но только в одном направлении (изменения в первой точке монтирования будут распространяться на вторую, но не наоборот);
private указывает, что изменения в первой точке монтирования не будут отображаться во второй, и наоборот;
rshared — то же, что shared, распространяет подобное поведения на все реплики точек монтирования;
rslave — то же, что slave, распространяет подобное поведения на все реплики точек монтирования;
rprivate (значение по умолчанию) — то же, что private, распространяет подобное поведения на все реплики точек монтирования;

Пример:

docker run -d \
-it \
--name devtest \
--mount type=bind,source="$(pwd)"/app/src,target=/app \
--mount type=bind,source="$(pwd)"/app/src,target=/app2,readonly,bind-propagation=rslave \
node:lts

Папка /app/src на хосте дважды монтируется к разным папкам в контейнере. Вторая точка монтирования имеет дополнительные настройки:

— приложение app2 может только читать данные из папки на хосте;
— изменения из первой точки монтирования сразу происходят и во вторую, но не наоборот.

Ключ bind-propagation служит для управления хранилищами на продвинутом уровне и, как правило, нужен в специальных задачах. Об этом механизме вы можете почитать подробнее в официальной документации Linux.

Флаг --mount не поддерживает опции для управления метками selinux (z и Z).

Проверьте корректность работы хранилища с помощью команды:

docker inspect devtest

В соответствующей секции Mounts вы сможете найти исчерпывающую информацию. Например, если вы находились в папке /tmp/source/target при запуске контейнера, то в этой секции будет указана примерно следующая информация:

"Mounts": [
{
"Type": "bind",
"Source": "/tmp/source/target",
"Destination": "/app",
"Mode": "",
"RW": true,
"Propagation": "rprivate"
}
],

Для разрыва связи между папками на хосте и в контейнере выполните команды остановки и удаления контейнера:

docker container stop devtest
docker container rm devtest

::callout ☝️

Помните:

  1. Связанными папками нельзя управлять из Docker CLI.
  2. Абсолютные пути на разных компьютерах могут быть разными.
  3. Если в контейнере в примонтированной папке есть содержимое, то оно «перекроет» содержимое связанной папки на все время работы контейнера.
  4. Использовать связанные папки для работы с конфигурационными файлами не безопасно.
  5. Файловая система и структура папок могут сильно отличаться на разных компьютерах.
  6. Правила описания путей к файлам могут отличаться при переходе от одной платформы к другой.
  7. Вы можете столкнуться с ситуацией, когда приложение в контейнере получит доступ к системным папкам или удалит критически важные файлы.

:::

Тома (volumes) 🔗

Тома являются лучшим типом управления данных в Docker. Только объекты или службы Docker должны иметь права на изменение данных, расположенных в томах. На хосте данные храняться в специальных папках, но без доступа администратора к ним не подобраться. В идеологии Docker тома — это некое подобие образов флэш-накопителя или CD/DVD.

Тома можно размещать не только на хосте. Можно, например, пользоваться облачными платформами для совместной работы с данными или для тестирования приложений. А еще тома будут работать как с Linux-контейнерами, так и с Windows-контейнерами, поскольку файловая система томов одна и та же.

Когда том примонтирован к контейнеру, операционная система хоста не имеет к нему доступа. Docker управляет томами отдельно, позволяя подключаться одному или нескольким контейнерам одновременно. Плюсом является и то, что том существует самостоятельно и не зависит от жизненного цикла контейнеров.

Тома могут быть созданы при сборке контейнера (с помощью Dockerfile или Docker Compose) или вручную с помощью Docker Engine. Тома могут иметь имя, назначенное пользователем (именованные тома / named volumes), а могут быть анонимными с именем, которое Docker устанавливает автоматически (анонимные тома / anonymous volumes).

Концепция драйвера позволяет преобразовывать данные в томах или влиять на потоки данных между томами и контейнерами. Например, это можно использовать для шифрования. Но чаще с помощью драйверов к контейнеру подключают тома, которые расположены не локально на хосте, а в облаке или на сервере. Это позволяет не меняя логику работы приложения внутри контейнера обрабатывать данные, которых на хосте нет.

Когда используем 🔗

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

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

Используем тома когда вы хотите хранить данные не только у себя на локальном компьютере, но и на сервере или в облаке.

Используем тома когда нужно создать резервную копию или перенести тома с одного компьютера на другой. Тома хранятся в определенной папке на компьютере. Вы можете просто скопировать ее, заархивировать и перенести на другой хост. Примерно также создается и резервная копия.

Используем тома если ваше приложение требует высокой скорости обмена данными на Mac и Windows. Тома сохраняются на виртуальной машине Linux VM, на которой работают и контейнеры, поэтому скорость чтения и записи высокая. Нет лишних накладных расходов на доступ к файловой системе хоста.

Используем тома когда важно, чтобы файловая система имела нативное поведение. Например, база данных должна контролировать кэширование на диске для гарантия выполнения транзакций. Файловые системы на Mac и Windows работают не так, как на Linux. Это может привести к ошибкам работы некоторых приложений.

Как используем 🔗

Тома являются предпочтительным способом управления данными в Docker.

Возможности:

— миграция данных и создание резервных копий;
— управление с помощью Docker CLI или Docker API;
— тома работают и с Linux, и с Windows контейнерами;
— данные легко и безопасно можно использовать в нескольких контейнерах;
— существует механизм драйверов, который позволяет хранить данные не только на хосте, но и на сервере или в облаке, шифровать данные в томе или добавлять дополнительную функциональность;
— новые тома могут создаваться с уже загруженными с помощью контейнера данными;
— если на хосте установлены Mac или Windows тома будут быстрее работать с Docker Desktop, чем связанные папки;
— тома не увеличивают размер контейнера;
— тома находятся вне жизненного цикла контейнера.

Создать том можно с помощью флагов -v или --mount при запуске контейнера. Для флага -v можно указать параметр ro, которое будет означать использование режима только для чтения. Для флага --mount есть ключ volume-opt, который устанавливает набор опций, разделенных запятыми. Не забывайте, что значение для этого ключа должны быть экранированы кавычками. Работа с томами такова, что изменения в одной точке монтирования в контейнере не будут отображаться в другой точке монтирования (параметр bind-propagation всегда выставлен в значение rprivate).

Подключить том с именем my-vol можно следующим образом:

— с флагом --mount:

docker run -d \
--name devtest \
--mount source=my-vol,target=/app \
node:lts

— с флагом -v:

docker run -d \
--name devtest \
-v my-vol:/app \
node:lts

Проверьте корректность результата выполненния команды:

docker inspect devtest

Чтобы удалить том необходимо отключить связанный с ним контейнер и удалить сам контейнер:

docker container stop devtest
docker container rm devtest
docker volume rm my-vol

Управлять томами можно через Docker API с помощью Docker CLI и Docker Compose.

Управление томами с Docker CLI 🔗

Чтобы создать новый том используйте команду:

docker volume create my-vol

Получите список томов на хосте:

docker volume ls

Посмотрите информацию о томе:

docker volume inspect my-vol

Удалите том командой:

docker volume rm my-vol

Если том был анонимным, то можно удалить его сразу после завершения работы контейнера. Для этого при запуске контейнера вы можете прописать флаг --rm. Вместе с удалением контейнера в этом случае удалится и том:

docker run --rm -v /foo -v awesome:/bar container app

После завершения работы и последующего удаления контейнера анонимный том удалится, а именованный awesome продолжит работать.

Чтобы удалить все неиспользуемые тома используйте команду:

docker volume prune

Управление томами с Dockerfile 🔗

Для того, чтобы подключить том с помощью Dockerfile необходимо использовать инструкцию VOLUME:

FROM node:lts
RUN useradd user
RUN mkdir /data && touch /data/x
RUN chown -R user:user /data
VOLUME /data

Интересно, что вы не сможете внести какие-либо изменения в данные на этапе сборки образа. Следующий Dockerfile правильно работать не будет:

FROM node:lts
RUN useradd user
VOLUME /data
RUN touch /data/x
RUN chown -R user:user /data

Том будет подключен только после создания образа на этапе запуска контейнера. Возможно придется использовать инструкции CMD или ENTRYPOINT. Подробнее описано в статье «Как устроен Dockerfile».

Управление томами с Docker Compose 🔗

Подробнее о формате конфигурации Docker Compose можно прочитать здесь.

Запустить том для отдельного контейнера можно с помощью следующей конфигурации:

services:
  frontend:
    image: node:lts
    volumes:
      - myapp:/home/node/app
volumes:
  myapp:

Команда docker-compose up поднимет не только сам контейнер frontend, но и создаст том myapp. Если он уже был создан, Docker Compose подключит его к контейнеру, но надо указать это явно с помощью элемента external так:

services:
  frontend:
    image: node:lts
    volumes:
      - myapp:/home/node/app
volumes:
  myapp:
    external: true

Использование драйверов 🔗

Когда настает пора масштабировать приложение, возникает необходимость одновременной работы нескольких сервисов с одним хранилищем данных. Есть масса решений для этого, и у Docker есть свое — драйверы для томов. Это лишь один пример использования драйверов. Можно организовать, например, пересылку данных между контейнерами с поддержкой шифрования или автоматическое шифрование / дешифровку всех данных в томе. Можно реализовать любой механизм обработки данных. Драйверы повышают уровень абстракции, позволяя отделить логику работы приложения от системы хранения данных.

Например, есть два компьютера — хост, на котором установлен Docker и запускаются контейнеры, и файловый сервер, который поставляет данные для них. Контейнеры ничего не знают про эту архитектуру: все запускалось изначально на локальном хосте. Драйвер vieux/sshfs позволяет использовать ssh-соединение для связи с файловым сервером, при этом данные будут представлены в виде тома Docker.

Для начала необходимо установить соответсвующий плагин для Docker Engine:

docker plugin install --grant-all-permissions vieux/sshfs

Затем нужно создать том и прописать учетные данные:

docker volume create --driver vieux/sshfs \
-o sshcmd=test@node2:/home/test \
-o password=testpassword \
sshvolume

Если для связи по SSH между клиентом и сервером уже работают ключи доступа, то пароль можно опустить. Флаг -o указывает на опции, которые могут быть переданы драйверу. Набор доступных опций у каждого драйвера свой.

Можно создать том и другим способом, при запуске контейнера:

docker run -d \
--name sshfs-container \
--volume-driver vieux/sshfs \
--mount src=sshvolume,target=/app,volume-opt=sshcmd=test@node2:/home/test,volume-opt=password=testpassword \
nginx:latest

Если драйвер требует передачи опций, приходится использовать флаг --mount.

Резервные копии 🔗

Для того, чтобы создать резервную копию тома, можно использовать механизм контейнеров Docker. Например, вы уже создали контейнер с именем dbstore на базе операционной системы Ubuntu и работаете с данными в томе dbdata. Для этого вы уже выполнили команду и получили доступ к терминалу контейнера:

docker run -v /dbdata --name dbstore node:lts /bin/bash

Как создать резервную копию данных в томе? Нужно:

— запустить новый контейнер и примонтировать том, который используется в контейнере dbstore;
— примонтировать папку на хосте, чтобы потом в нее положить резервную копию;
— зайти внутри контейнера в том, заархивировать данные и положить их в связанную папку.

Выполните команду:

docker run --rm --volumes-from dbstore -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

После завершения архивации контейнер выключится и удалится, а резервная копия останется у вас в папке, из которой вы запускали команду.

Допустим, у вас возникла необходимость развернуть данные из сохраненной резервной копии внутри контейнера dbstore2. Нужно запустить его:

docker run -v /dbdata --name dbstore2 node:lts /bin/bash

Затем разархивировать данные в том:

docker run --rm --volumes-from dbstore2 -v $(pwd):/backup ubuntu bash -c "cd /dbdata && tar xvf /backup/backup.tar --strip 1"

Хранение в оперативной памяти (tmpfs mounts или npipe mounts) 🔗

Механизм tmpfs mount в операционной системе Linux позволяет выделить часть оперативной памяти хоста для хранения данных. Данные не сохраняются в файловой системе и получается быстрое хранилище. Примонтированная папка tmpfs работает, пока существует контейнер, поэтому не стоит использовать этот способ для хранения настроек и результатов работы приложения.

Для пользователей операционной системы Windows существует еще один тип управления данными — npipe mount. Этот тип позволяет получить доступ к хосту Docker из контейнера и в основном используется для управления данными с Docker Engine API.

Когда используем 🔗

Используем оперативную память если вы не хотите оставлять данные после завершения работы приложения.

Как используем (только на Linux) 🔗

С помощью томов и связанных папок вы можете делиться файлами между хостом и контейнером. После остановки контейнера данные сохраняются. Но если на хосте используется операционная система Linux, то существует и третий тип работы с данными — tmpfs. Это временное файловое хранилище, которое располагается в оперативной памяти, присутствует во многих Unix-подобных системах. Когда вы создаете контейнер, Docker может создать отдельный слой в оперативной памяти снаружи контейнера для хранения и обработки данных.

При использовании этого типа работы с данными в Docker есть два ограничения:

— операционной системой хоста может быть только Linux;
— данные в tmpfs доступны лишь из одного контейнера.

tmpfs хорошо работает в случае хранения чувствительной информации: ключей шифрования, паролей, сертификатов доступа и тому подобное.

Чтобы запустить контейнер с tmpfs используют команду:

docker run -d \
-it \
--name tmptest \
--mount type=tmpfs,destination=/app \
node:lts

С помощью ключа tmpfs-size можно определить максимальный размер хранилища в байтах. По умолчанию он не ограничен. Ключ tmpfs-mode служит для определения уровня доступа в восьмеричном формате. Например, значение по умолчанию 1777 обозначает, что любой пользователь или программа в контейнере имеет неограниченный доступ к данным, которые будут доступны и вне контейнера. Этот параметр работает также, как и для tmpfs в Unix-подобных операционных системах.

Также есть альтернативная более короткая команда для управления tmpfs mounts:

docker run -d \
-it \
--name tmptest \
--tmpfs /app \
node:lts

Проверьте состояние контейнера, чтобы убедиться, что файловое хранилище создано корректно:

docker container inspect tmptest

В соответствующей секции будет доступна информация о примонтированной папке:

"Tmpfs": {
"/app": ""
},

Для удаления слоя с данными выполните команды остановки и удаления контейнера:

docker container stop tmptest
docker container rm tmptest

В работе 🔗

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

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

Управление томами, как правило, осуществляется отдельно от контейнеров с помощью Docker CLI. Связанные папки и tmpfs mounts создаются при запуске контейнера командой docker run или с помощью конфигурации в Docker Compose. Существует два варианта управления хранилищами с помощью команды docker run:

-- флаг -v или --volume;
-- флаг --mount.

Новых пользователей команда Docker призывает использовать второй вариант в качестве приоритетного. Он более информативен и понятен, работает для всех типов управления данными в Docker. Однако есть особенности использования каждого флага, тонкости, о которых нельзя забывать. Например, для bind mounts флаг -v создает папку на хосте, если ее до этого не было. Флаг --mount выдаст ошибку в этом случае и папку не создаст. Для tmpfs mounts флаг -v вообще не используется. Кроме того, отличается и синтаксис.

Для флага -v существуют только три поля, которые указываются через символ двоеточия (:). В первом поле содержится имя тома или путь к связанной папке на хосте. Для анонимных томов, это поле остается пустым. Второе поле содержит путь к папке внутри контейнера, в которую монтируется хранилище. Третье поле опционально и содержит разделенный запятыми список специальных настроек.

После флага --mount список параметров состоит из пар типа ключ=значение, разделенных запятыми. Порядок следования пар не важен. Список параметров для каждого типа управления данными немного отличается, но есть и общая часть:

-- ключ type используется для указания типа управления данными и может принимать значения volumes, bind и tmpfs;
-- ключ source (или src) используется для именованных томов и для указания пути к папки для bind mounts;
-- значение для ключа destination (или dst, или target) содержит путь внутри контейнера, который определяет, куда должно быть примонтировано хранилище;
-- ключ readonly указывает, что в томе или связанной папке нельзя менять данные приложениям или скриптам из контейнера. Этот ключ не имеет значения и просто перечисляется среди прочих через запятую.