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

Время чтения: больше 15 мин

В этой статье мы поговорим про управление данными приложений в Docker. Узнать, что такое Docker, вы сможете из статьи «Что такое Docker». Также вы можете почитать о мультиконтейнерных приложениях и Docker Compose и о том, как устроен Dockerfile.

Итак, по умолчанию все данные приложения хранятся в контейнере Docker и после остановки контейнера теряются. Но это не единственный способ работать с данными. Можно использовать оперативную память и файловую систему компьютера, на котором установлен Docker Engine. Существует несколько типов хранилищ данных:

  • связанные папки, примонтированные к контейнеру как внешние диски (bind mounts);
  • тома (volumes);
  • часть оперативной памяти для работы с данными (tmpfs mounts или npipe mounts).

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

Наглядная схема типов управления данными в Docker:

Наглядная схема типов управления данными в Docker.

Рассмотрим каждый тип по отдельности.

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

Секция статьи "Связанные папки (bind mounts)"

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

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

Связанные папки используются:

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

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

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

Как пользоваться

Секция статьи "Как пользоваться"

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

Пример с флагом -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. Это приведёт к тому, что работа операционной системы на хосте будет парализована. Будьте аккуратны!

Пример с флагом --mount:

        
          
          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

Для флага --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
          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
          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)

Секция статьи "Тома (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 или --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
          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

        
        
          
        
      

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

        
          
          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-compose up поднимет не только сам контейнер 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/sshfs позволяет использовать 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

        
        
          
        
      

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

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

Секция статьи "Резервные копии"

Для того чтобы создать резервную копию тома, можно использовать механизм контейнеров 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-size можно определить максимальный размер хранилища в байтах. По умолчанию он не ограничен. Ключ tmpfs-mode служит для определения уровня доступа в восьмеричном формате. Например, значение по умолчанию 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-накопитель к уже заполненной чем-то папке.