В этой статье мы поговорим про управление данными приложений в Docker. Узнать, что такое Docker, вы сможете из статьи «Что такое Docker». Также вы можете почитать о мультиконтейнерных приложениях и Docker Compose и о том, как устроен Dockerfile.
Итак, по умолчанию все данные приложения хранятся в контейнере Docker и после остановки контейнера теряются. Но это не единственный способ работать с данными. Можно использовать оперативную память и файловую систему компьютера, на котором установлен Docker Engine. Существует несколько типов хранилищ данных:
- связанные папки, примонтированные к контейнеру как внешние диски (bind mounts);
- тома (volumes);
- часть оперативной памяти для работы с данными (tmpfs mounts или npipe mounts).
Вне зависимости от того, какой тип хранилища вы выберете, данные для приложения будут храниться в заданной вами папке внутри контейнера. Технология работает бесшовно, но имеет свои накладные расходы для каждого конкретного типа.
Наглядная схема типов управления данными в Docker:
Рассмотрим каждый тип по отдельности.
Связанные папки (bind mounts)
СкопированоСвязанные папки появились в Docker с самых первых релизов. Это удобный инструмент, но у него есть ограничения. Этот тип управления данными позволяет связать папку на компьютере пользователя (то есть хосте, на котором установлен Docker Engine) и папку в контейнере. Работать в контейнере и на хосте с такой папкой можно одновременно, все изменения будут отображаться и там, и там. Механизм bind mounts подразумевает, что данные могут быть изменены в любое время как из подключённого контейнера, так и непосредственно на хосте.
При создании связанной папки указывается полный путь к ней на хосте и путь внутри контейнера. Если папка не существует на хосте, Docker может создать её сам.
Связанные папки используются:
Когда конфигурационные файлы на хосте и в контейнере одни и те же. Именно этот тип использует сам Docker для автоматического монтирования конфигурации DNS хоста.
Когда работаем с исходным кодом и артефактами сборок. Можно использовать системы сборки для исходного кода внутри контейнера. Вы меняете код, бандлер, который находится внутри контейнера, это видит, и код попадает в новую сборку. Другой вариант использования — работа с уже собранными бандлами, например, для тестирования или отладки приложений.
Когда необходимо обеспечить создание одной и той же файловой структуры на различных компьютерах. Если папки на компьютере пользователя не существует, она будет создана при сборке образа и запуске контейнера.
Как пользоваться
СкопированоЧтобы связать папку на хосте с папкой внутри контейнера, можно воспользоваться флагами -v
или -
. $
в командах ниже означает, что примонтируется текущая папка на хосте.
Пример с флагом -v
:
docker run -d \ -it \ --name devtest \ -v "$(pwd)"/target:/app \ node:lts
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. Это приведёт к тому, что работа операционной системы на хосте будет парализована. Будьте аккуратны!
Пример с флагом -
:
docker run -d \ -it \ --name devtest \ --mount type=bind,source="$(pwd)"/target,target=/app \ node:lts
docker run -d \ -it \ --name devtest \ --mount type=bind,source="$(pwd)"/target,target=/app \ node:lts
Ключ bind-propagation
Для флага -
есть ключ bind
, который работает только на Linux (операционные системы контейнера и хоста должны поддерживать этот режим работы).
Представьте, есть две точки монтирования /mnt1
и /mnt2
, к которым привязана одна и та же папка на хосте. Значения ключа bind
определяют, что произойдёт, если в связанной папке появятся подпапки. Что произойдёт с /mnt2
при монтировании /mnt1
? Возможны следующие варианты:
— shared
указывает на то, что изменения для точки монтирования /mnt1
будут в точности отражаться в /mnt2
и наоборот;
— 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
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
служит для управления хранилищами на продвинутом уровне и, как правило, нужен в специальных задачах. Об этом механизме вы можете почитать подробнее в официальной документации Linux.
Флаг -
не поддерживает опции для управления метками selinux (z
и Z
).
Проверьте корректность работы хранилища с помощью команды:
docker inspect devtest
docker inspect devtest
В соответствующей секции Mounts вы сможете найти исчерпывающую информацию. Например, если вы находились в папке /tmp/source/target при запуске контейнера, то в этой секции будет указана примерно следующая информация:
"Mounts": [ { "Type": "bind", "Source": "/tmp/source/target", "Destination": "/app", "Mode": "", "RW": true, "Propagation": "rprivate" }],
"Mounts": [ { "Type": "bind", "Source": "/tmp/source/target", "Destination": "/app", "Mode": "", "RW": true, "Propagation": "rprivate" } ],
Для разрыва связи между папками на хосте и в контейнере выполните команды остановки и удаления контейнера:
docker container stop devtestdocker container rm devtest
docker container stop devtest docker container rm devtest
Тома (volumes)
СкопированоТома — это лучший тип управления данных в Docker. Только объекты или службы Docker должны иметь права на изменение данных, расположенных в томах. На хосте данные хранятся в специальных папках, но без доступа администратора к ним не подобраться. В идеологии Docker тома — что-то вроде образа флэш-накопителя или CD/DVD.
Тома можно размещать не только на хосте. Можно, например, пользоваться облачными платформами для совместной работы с данными или для тестирования приложений. А ещё тома будут работать как с Linux-контейнерами, так и с Windows-контейнерами, поскольку файловая система томов одна и та же.
Когда том примонтирован к контейнеру, операционная система хоста не имеет к нему доступа. Docker управляет томами отдельно, позволяя подключаться одному или нескольким контейнерам одновременно. Плюсом является и то, что том существует самостоятельно и не зависит от жизненного цикла контейнеров.
Тома могут быть созданы при сборке контейнера (с помощью Dockerfile или Docker Compose) или вручную с помощью Docker Engine. Тома могут иметь имя, назначенное пользователем (именованные тома, named volumes), а могут быть анонимными с именем, которое Docker устанавливает автоматически (анонимные тома, anonymous volumes).
Концепция драйвера позволяет преобразовывать данные в томах или влиять на потоки данных между томами и контейнерами. Например, это можно использовать для шифрования. Но чаще с помощью драйверов к контейнеру подключают тома, которые расположены не локально на хосте, а в облаке или на сервере. Это позволяет, не меняя логику работы приложения внутри контейнера, обрабатывать данные, которых на хосте нет.
Итак, возможности томов:
— миграция данных и создание резервных копий;
— управление с помощью Docker CLI или Docker API;
— тома работают и с Linux-, и с Windows-контейнерами;
— данные легко и безопасно можно использовать в нескольких контейнерах;
— существует механизм драйверов, который позволяет хранить данные не только на хосте, но и на сервере или в облаке, шифровать данные в томе или добавлять дополнительную функциональность;
— новые тома могут создаваться с уже загруженными с помощью контейнера данными;
— если на хосте установлены Mac или Windows, тома будут быстрее работать с Docker Desktop, чем связанные папки;
— тома не увеличивают размер контейнера;
— тома находятся вне жизненного цикла контейнера.
Тома используются:
Когда нам нужно получить доступ к данным из разных контейнеров. Том создаётся в первый раз либо вручную, либо при сборке контейнера. Уничтожается том всегда только с помощью Docker вручную. После остановки контейнера том будет продолжать работать, пока не будет удалён пользователем.
Когда вы не уверены, что путь до папки будет одним и тем же на разных компьютерах. Тома позволяют повысить уровень абстракции.
Когда вы хотите хранить данные не только у себя на локальном компьютере, но и на сервере или в облаке.
Когда нужно создать резервную копию или перенести тома с одного компьютера на другой. Тома хранятся в определённой папке на компьютере. Вы можете просто скопировать её, заархивировать и перенести на другой хост. Примерно так же создаётся и резервная копия.
Если ваше приложение требует высокой скорости обмена данными на Mac и Windows. Тома сохраняются на виртуальной машине Linux VM, на которой работают и контейнеры, поэтому скорость чтения и записи высокая. Нет лишних накладных расходов на доступ к файловой системе хоста.
Когда важно, чтобы файловая система имела нативное поведение. Например, база данных должна контролировать кэширование на диске для гарантии выполнения транзакций. Файловые системы на Mac и Windows работают не так, как на Linux. Это может привести к ошибкам работы некоторых приложений.
Как пользоваться
СкопированоСоздать том можно с помощью флагов -v
или -
при запуске контейнера. Для флага -v
можно указать параметр ro
, который будет означать использование режима только для чтения. Для флага -
есть ключ volume
, который устанавливает набор опций, разделённых запятыми. Не забывайте, что значения для этого ключа должны быть экранированы кавычками. Работа с томами такова, что изменения в одной точке монтирования в контейнере не будут отображаться в другой точке монтирования (параметр bind
всегда выставлен в значение rprivate
).
Подключить том с именем my
можно следующим образом.
С флагом -
:
docker run -d \ --name devtest \ --mount source=my-vol,target=/app \ node:lts
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 run -d \ --name devtest \ -v my-vol:/app \ node:lts
Проверьте корректность результата выполнения команды:
docker inspect devtest
docker inspect devtest
Чтобы удалить том, необходимо отключить связанный с ним контейнер и удалить сам контейнер:
docker container stop devtestdocker container rm devtestdocker volume rm my-vol
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 create my-vol
Получите список томов на хосте:
docker volume ls
docker volume ls
Посмотрите информацию о томе:
docker volume inspect my-vol
docker volume inspect my-vol
Удалите том командой:
docker volume rm my-vol
docker volume rm my-vol
Если том был анонимным, то можно удалить его сразу после завершения работы контейнера. Для этого при запуске контейнера вы можете прописать флаг -
. Вместе с удалением контейнера в этом случае удалится и том:
docker run --rm -v /foo -v awesome:/bar container app
docker run --rm -v /foo -v awesome:/bar container app
После завершения работы и последующего удаления контейнера анонимный том удалится, а именованный awesome
продолжит работать.
Чтобы удалить все неиспользуемые тома, используйте команду:
docker volume prune
docker volume prune
Для того, чтобы подключить том с помощью Dockerfile, необходимо использовать инструкцию VOLUME
:
FROM node:ltsRUN useradd userRUN mkdir /data && touch /data/xRUN chown -R user:user /dataVOLUME /data
FROM node:lts RUN useradd user RUN mkdir /data && touch /data/x RUN chown -R user:user /data VOLUME /data
Интересно, что вы не сможете внести какие-либо изменения в данные на этапе сборки образа. Следующий Dockerfile правильно работать не будет:
FROM node:ltsRUN useradd userVOLUME /dataRUN touch /data/xRUN chown -R user:user /data
FROM node:lts RUN useradd user VOLUME /data RUN touch /data/x RUN chown -R user:user /data
Том будет подключён только после создания образа на этапе запуска контейнера. Возможно, придётся использовать инструкции CMD
или ENTRYPOINT
. Подробнее описано в статье «Как устроен Dockerfile».
Запустить том для отдельного контейнера с Docker Compose можно с помощью следующей конфигурации:
services: frontend: image: node:lts volumes: - myapp:/home/node/appvolumes: myapp:
services: frontend: image: node:lts volumes: - myapp:/home/node/app volumes: myapp:
Команда docker
поднимет не только сам контейнер frontend
, но и создаст том myapp
. Если он уже был создан, Docker Compose подключит его к контейнеру, но надо указать это явно с помощью элемента external
так:
services: frontend: image: node:lts volumes: - myapp:/home/node/appvolumes: myapp: external: true
services: frontend: image: node:lts volumes: - myapp:/home/node/app volumes: myapp: external: true
Подробнее о формате конфигурации Docker Compose можно прочитать в статье о Docker Compose.
Использование драйверов
СкопированоКогда приходит время масштабировать приложение, несколько сервисов должны работать с одним хранилищем данных. Для этого существует масса решений, и у Docker есть своё — драйверы для томов. Это лишь один пример использования драйверов. Можно организовать, например, пересылку данных между контейнерами с поддержкой шифрования или автоматическое шифрование и дешифровку всех данных в томе. Можно реализовать любой механизм обработки данных. Драйверы повышают уровень абстракции, позволяя отделить логику работы приложения от системы хранения данных.
Например, есть два компьютера — хост, на котором установлен Docker и запускаются контейнеры, и файловый сервер, который поставляет данные для них. Контейнеры ничего не знают про эту архитектуру: все запускалось изначально на локальном хосте. Драйвер vieux
позволяет использовать SSH-соединение для связи с файловым сервером, при этом данные будут представлены в виде тома Docker.
Для начала необходимо установить соответствующий плагин для Docker Engine:
docker plugin install --grant-all-permissions vieux/sshfs
docker plugin install --grant-all-permissions vieux/sshfs
Затем нужно создать том и прописать учётные данные:
docker volume create --driver vieux/sshfs \ -o sshcmd=test@node2:/home/test \ -o password=testpassword \ sshvolume
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
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
Если драйвер требует передачи опций, приходится использовать флаг -
.
Резервные копии
СкопированоДля того чтобы создать резервную копию тома, можно использовать механизм контейнеров Docker. Например, вы уже создали контейнер с именем dbstore
на базе операционной системы Ubuntu и работаете с данными в томе dbdata
. Для этого вы уже выполнили команду и получили доступ к терминалу контейнера:
docker run -v /dbdata --name dbstore node:lts /bin/bash
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
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 -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"
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
docker run -d \ -it \ --name tmptest \ --mount type=tmpfs,destination=/app \ node:lts
С помощью ключа tmpfs
можно определить максимальный размер хранилища в байтах. По умолчанию он не ограничен. Ключ tmpfs
служит для определения уровня доступа в восьмеричном формате. Например, значение по умолчанию 1777
обозначает, что любой пользователь или программа в контейнере имеют неограниченный доступ к данным, которые будут доступны и вне контейнера. Этот параметр работает также, как и для tmpfs в Unix-подобных операционных системах.
Также есть альтернативная более короткая команда для управления tmpfs mounts:
docker run -d \ -it \ --name tmptest \ --tmpfs /app \ node:lts
docker run -d \ -it \ --name tmptest \ --tmpfs /app \ node:lts
Проверьте состояние контейнера, чтобы убедиться, что файловое хранилище создано корректно:
docker container inspect tmptest
docker container inspect tmptest
В соответствующей секции будет доступна информация о примонтированной папке:
"Tmpfs": { "/app": ""},
"Tmpfs": { "/app": "" },
Для удаления слоя с данными выполните команды остановки и удаления контейнера:
docker container stop tmptestdocker container rm tmptest
docker container stop tmptest docker container rm tmptest
На практике
Скопированосоветует Скопировано
✨ Вы можете получить данные из определённой папки контейнера, если примонтируете к ней пустую папку или пустой том на хосте. После монтирования данные автоматически окажутся доступны с хоста и останутся там после работы контейнера. Это удобный способ сделать бэкап или получить результаты работы приложения.
Если папка или том окажутся не пустыми, то при монтировании к контейнеру содержимое будет на время скрыто. Контейнер будет воспринимать эту папку как пустую, данные будут в неё сохраняться и будут доступны с хоста во время работы контейнера. После окончания работы контейнера или после того, как том или папка будут отмонтированы, данные из контейнера будут потеряны, поскольку снова будут доступны те файлы и подпапки, которые были скрыты при монтировании. Это тот же механизм, который Linux будет использовать, когда вы, например, примонтируете USB-накопитель к уже заполненной чем-то папке.
На собеседовании
СкопированоЭто вопрос без ответа. Вы можете помочь! Почитайте о том, как контрибьютить в Доку.