Как обмениваться данными между контейнерами Docker
Введение
Docker — это популярный инструмент контейнеризации, используемый для предоставления программным приложениям файловой системы, содержащей все необходимое для запуска. Использование контейнеров Docker гарантирует, что программное обеспечение будет вести себя одинаково независимо от того, где оно развернуто, поскольку его среда выполнения непротиворечива.
Как правило, контейнеры Docker эфемерны и работают ровно столько, сколько требуется для выполнения команды, выданной в контейнере. Однако иногда приложениям необходимо предоставлять общий доступ к данным или сохранять данные после удаления контейнера. Базы данных, пользовательский контент для веб-сайта и файлы журналов — это лишь несколько примеров данных, которые нецелесообразно или невозможно включать в образ Docker, но к которым приложения должны иметь доступ. Постоянный доступ к данным обеспечивается Docker Volumes.
Тома Docker можно создавать и присоединять с помощью той же команды, которая создает контейнер, или их можно создавать независимо от каких-либо контейнеров и присоединять позже. В этой статье мы рассмотрим четыре различных способа обмена данными между контейнерами.
Предпосылки
Чтобы следовать этой статье, вам понадобится сервер Ubuntu 20.04 со следующим:
- Пользователь без полномочий root с привилегиями sudo. В руководстве Initial Server Setup with Ubuntu 20.04 объясняется, как это настроить.
- Docker установлен в соответствии с инструкциями из шагов 1 и 2 раздела «Как установить и использовать Docker в Ubuntu 20.04».
Примечание. Несмотря на то, что в предварительных требованиях содержатся инструкции по установке Docker в Ubuntu 20.04, команды docker
для томов данных Docker в этой статье должны работать в других операционных системах, если установлен Docker и пользователь sudo был добавлен в группу docker
.
Шаг 1 — Создание независимого тома
Представленная в выпуске Docker 1.9 команда docker volume create
позволяет создать том, не связывая его с каким-либо конкретным контейнером. Мы будем использовать эту команду для добавления тома с именем DataVolume1
:
- docker volume create --name DataVolume1
Отображается имя, указывающее, что команда выполнена успешно:
OutputDataVolume1
Чтобы использовать том, мы создадим новый контейнер из образа Ubuntu, используя флаг --rm
, чтобы автоматически удалить его при выходе. Мы также будем использовать -v
для монтирования нового тома. -v
требует имя тома, двоеточие, а затем абсолютный путь к тому месту, где том должен появиться внутри контейнера. Если каталоги в пути не существуют как часть образа, они будут созданы при выполнении команды. Если они действительно существуют, смонтированный том скроет существующее содержимое:
- docker run -ti --rm -v DataVolume1:/datavolume1 ubuntu
Находясь в контейнере, давайте запишем некоторые данные в том:
- echo "Example1" > /datavolume1/Example1.txt
Поскольку мы использовали флаг --rm
, наш контейнер будет автоматически удален при выходе. Однако наш том по-прежнему будет доступен.
- exit
Мы можем убедиться, что том присутствует в нашей системе с помощью docker volume inspect
:
- docker volume inspect DataVolume1
Output[
{
"CreatedAt": "2018-07-11T16:57:54Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/DataVolume1/_data",
"Name": "DataVolume1",
"Options": {},
"Scope": "local"
}
]
Примечание. Мы можем даже просмотреть данные на хосте по пути, указанному как Mountpoint
. Однако нам следует избегать его изменения, поскольку это может привести к повреждению данных, если приложения или контейнеры не знают об изменениях.
Далее запустим новый контейнер и присоединим DataVolume1
:
- docker run --rm -ti -v DataVolume1:/datavolume1 ubuntu
Проверьте содержимое:
- cat /datavolume1/Example1.txt
OutputExample1
Выход из контейнера:
- exit
В этом примере мы создали том, присоединили его к контейнеру и проверили его постоянство.
Шаг 2 — Создание тома, который сохраняется при удалении контейнера
В нашем следующем примере мы создадим том одновременно с контейнером, удалим контейнер, а затем присоединим том к новому контейнеру.
Мы будем использовать команду docker run
для создания нового контейнера с использованием базового образа Ubuntu. -t
даст нам терминал, а -i
позволит нам взаимодействовать с ним. Для ясности мы будем использовать --name
для идентификации контейнера.
Флаг -v
позволит нам создать новый том, который мы назовем DataVolume2
. Мы будем использовать двоеточие, чтобы отделить это имя от пути, по которому том должен быть смонтирован в контейнере. Наконец, мы укажем базовый образ Ubuntu и полагаемся на команду по умолчанию в файле Docker базового образа Ubuntu, bash
, чтобы поместить нас в оболочку:
- docker run -ti --name=Container2 -v DataVolume2:/datavolume2 ubuntu
Примечание. Флаг -v
очень гибкий. Он может связать или назвать том с небольшой корректировкой синтаксиса. Если первый аргумент начинается с /
или ~/
, вы создаете привязку. Удалите это, и вы назовете том. Например:
-v /path:/path/in/container
монтирует каталог хоста,/path
в/path/in/container
-v path:/path/in/container
создает том с именемpath
, не связанный с хостом.
Дополнительные сведения о привязке каталога с хоста см. в разделе Как обмениваться данными между контейнером Docker и хостом.
Находясь в контейнере, мы запишем некоторые данные в том:
- echo "Example2" > /datavolume2/Example2.txt
- cat /datavolume2/Example2.txt
OutputExample2
Выходим из контейнера:
- exit
Когда мы перезапустим контейнер, том смонтируется автоматически:
- docker start -ai Container2
Давайте проверим, что том действительно смонтирован и наши данные все еще на месте:
- cat /datavolume2/Example2.txt
OutputExample2
Наконец, давайте выйдем и очистим:
- exit
Docker не позволит нам удалить том, если на него ссылается контейнер. Давайте посмотрим, что произойдет, когда мы попытаемся:
- docker volume rm DataVolume2
Сообщение сообщает нам, что том все еще используется, и предоставляет длинную версию идентификатора контейнера:
OutputError response from daemon: unable to remove volume: remove DataVolume2: volume is in use - [d0d2233b668eddad4986313c7a4a1bc0d2edaf0c7e1c02a6a6256de27db17a63]
Мы можем использовать этот идентификатор для удаления контейнера:
- docker rm d0d2233b668eddad4986313c7a4a1bc0d2edaf0c7e1c02a6a6256de27db17a63
Outputd0d2233b668eddad4986313c7a4a1bc0d2edaf0c7e1c02a6a6256de27db17a63
Удаление контейнера не повлияет на объем. Мы можем увидеть, что он все еще присутствует в системе, перечислив тома с помощью docker volume ls
:
- docker volume ls
OutputDRIVER VOLUME NAME
local DataVolume2
И мы можем использовать docker volume rm
, чтобы удалить его:
- docker volume rm DataVolume2
В этом примере мы создали пустой том данных одновременно с созданием контейнера. В нашем следующем примере мы рассмотрим, что происходит, когда мы создаем том с каталогом контейнера, который уже содержит данные.
Шаг 3 — Создание тома из существующего каталога с данными
Как правило, независимое создание тома с помощью docker volume create
и создание его при создании контейнера эквивалентны, за одним исключением. Если мы создаем том одновременно с созданием контейнера и указываем путь к каталогу, содержащему данные в базовом образе, эти данные будут скопированы в том.
В качестве примера мы создадим контейнер и добавим том данных в /var
, каталог, который содержит данные в базовом образе:
- docker run -ti --rm -v DataVolume3:/var ubuntu
Весь контент из каталога /var
базового образа копируется в том, и мы можем смонтировать этот том в новый контейнер.
Выйти из текущего контейнера:
- exit
На этот раз вместо того, чтобы полагаться на команду bash
по умолчанию из базового образа, мы выполним собственную команду ls
, которая покажет содержимое тома без входа в оболочку:
- docker run --rm -v DataVolume3:/datavolume3 ubuntu ls datavolume3
Каталог datavolume3
теперь содержит копию содержимого каталога /var
базового образа:
Outputbackups
cache
lib
local
lock
log
mail
opt
run
spool
tmp
Маловероятно, что мы захотим монтировать /var/
таким образом, но это может быть полезно, если мы создали собственный образ и нам нужен простой способ сохранения данных. В нашем следующем примере мы продемонстрируем, как том может быть разделен между несколькими контейнерами.
Шаг 4 — Совместное использование данных между несколькими контейнерами Docker
До сих пор мы присоединяли том к одному контейнеру за раз. Часто нам нужно, чтобы несколько контейнеров были подключены к одному и тому же объему данных. Это относительно просто сделать, но есть одно важное предостережение: в настоящее время Docker не обрабатывает блокировку файлов. Если вам нужно, чтобы несколько контейнеров выполняли запись на том, приложения, работающие в этих контейнерах, должны быть спроектированы для записи в общие хранилища данных, чтобы предотвратить повреждение данных.
Создайте Container4 и DataVolume4
Используйте docker run
, чтобы создать новый контейнер с именем Container4
с присоединенным томом данных:
- docker run -ti --name=Container4 -v DataVolume4:/datavolume4 ubuntu
Далее мы создадим файл и добавим текст:
- echo "This file is shared between containers" > /datavolume4/Example4.txt
Затем мы выйдем из контейнера:
- exit
Это возвращает нас к командной строке хоста, где мы создадим новый контейнер, который монтирует том данных из Container4
.
Создайте Container5 и подключите тома из Container4
Мы создадим Container5
и смонтируем тома из Container4
:
- docker run -ti --name=Container5 --volumes-from Container4 ubuntu
Проверим сохраняемость данных:
- cat /datavolume4/Example4.txt
OutputThis file is shared between containers
Теперь давайте добавим текст из Container5
:
- echo "Both containers can write to DataVolume4" >> /datavolume4/Example4.txt
Наконец, мы выйдем из контейнера:
- exit
Далее мы проверим, что наши данные все еще присутствуют в Container4
.
Просмотр изменений, сделанных в Container5
Давайте проверим изменения, которые были записаны в том данных с помощью Container5
, перезапустив Container4
:
- docker start -ai Container4
Проверьте изменения:
- cat /datavolume4/Example4.txt
OutputThis file is shared between containers
Both containers can write to DataVolume4
Теперь, когда мы убедились, что оба контейнера могут читать и записывать из тома данных, мы выйдем из контейнера:
- exit
Опять же, Docker не обрабатывает блокировку файлов, поэтому приложения должны учитывать блокировку файлов сами. Можно смонтировать том Docker как доступный только для чтения, чтобы гарантировать, что повреждение данных не произойдет случайно, когда контейнеру требуется доступ только для чтения, добавив :ro
. Давайте посмотрим, как это работает.
Запустите контейнер 6 и смонтируйте том только для чтения
После того, как том был смонтирован в контейнере, вместо того, чтобы размонтировать его, как в случае с типичной файловой системой Linux, мы можем вместо этого создать новый контейнер, смонтированный так, как мы хотим, и, при необходимости, удалить предыдущий контейнер. Чтобы сделать том доступным только для чтения, мы добавляем :ro
в конец имени контейнера:
- docker run -ti --name=Container6 --volumes-from Container4:ro ubuntu
Мы проверим статус «только для чтения», попытавшись удалить файл нашего примера:
- rm /datavolume4/Example4.txt
Outputrm: cannot remove '/datavolume4/Example4.txt': Read-only file system
Наконец, мы выйдем из контейнера и очистим наши тестовые контейнеры и тома:
- exit
Теперь, когда мы закончили, давайте очистим наши контейнеры и том:
- docker rm Container4 Container5 Container6
- docker volume rm DataVolume4
В этом примере мы показали, как обмениваться данными между двумя контейнерами с помощью тома данных и как смонтировать том данных как доступный только для чтения.
Заключение
В этом руководстве мы создали том данных, который позволял сохранять данные при удалении контейнера. Мы делили объемы данных между контейнерами с оговоркой, что приложения должны быть спроектированы так, чтобы обрабатывать блокировку файлов для предотвращения повреждения данных. Наконец, мы показали, как смонтировать общий том в режиме только для чтения. Если вам интересно узнать об обмене данными между контейнерами и хост-системой, см. раздел Как обмениваться данными между контейнером Docker и хостом.