04 июня 2022


Создаем volume в Docker используя bind и mount на примерах


Запуская контейнер Docker нам может понадобится сохранить где-то данные или наоборот добавить их в контейнер. Для реализации этой задачи, в Docker, был создан объект томов и возможность проброса папок. Рассмотрим как это работает на примерах.

 

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

Понимание надобности проброса папок и создания томов появляется при первом ознакомлении работы контейнеров в целом.

Если у вас есть файл "code.py", который подразумевает работу какого-то приложения, вы можете положить его в образ (image), но это создаст некоторые проблемы. Например вам нужно будет выполнять пересоздание образа (build) каждый раз, как "code.py" изменится. Сборка образа может происходить десятки минут. Образ Docker становится read-only после его создания т.е. не рассчитан на изменения.

Если вы не положили "code.py" в образ, а решили скопировать его внутрь контейнера - это так же создаст проблему. Контейнер является дополнительным слоем/snapshot над выбранным образом и имеет возможность записи. Время жизни контейнера равно времени жизни сервису, который запущен внутри него. Т.е. если у вас будет ошибка в приложении, то вам нужно будет пересоздавать контейнер и копировать файл еще раз. Все еще больше усугубиться, если вы запускаете 10 контейнеров, а вес файлов исчисляется в Гб.

Слой для записи в Docker

Похожая проблема есть, если вы хотите получить данные из контейнера (например логи). Вы можете написать скрипт, который будет копировать большой объем файлов с 1 или 100 контейнеров, но этой будет занимать много времени.

Проброс папок и томов в Docker решает эти проблемы позволяя монтировать директории хоста внутри контейнера либо создавая централизованное хранилище. Таким образом мы получаем следующие преимущества:

  1. Мы не копируем данные, они хранятся в одно месте для всех контейнеров;
  2. Т.к. копирование отнимало время, а сейчас это делать не нужно, контейнеры запускаются быстрее;
  3. У нас появляется больше возможностей для управления данными.

 

Типы томов Docker

Есть два основных способа обмена данными с контейнером, которые часто называют томами:

  • перенаправление какой-то папки или файла с хоста в контейнер (так же называется bind mount);
  • создание специального объекта - volume (так же называется named volume), который имеет больше возможностей управления данными через Docker.

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

Еще одно, незначительно, отличие это поведение по умолчанию bind mount и volume. Для примера, внутри контейнера, по пути "/usr/share/nginx/html/" лежит файл "index.html". В случае проброса томов в эту папку поведение будет разным:

  • В случае монтирования папки - "index.html", внутри контейнера, будет удален. Это произойдет даже если папка хоста пустая;
  • В случае volume - при первом использовании тома файл "index.html" будет скопирован. При последующих - удален.

Есть еще один тип томов - tmpfs, который работает только под Linux. Он подразумевает хранение данных в ОЗУ и с ограничением в 1 контейнер.

Типы томов Docker

 

 

Монтирование через docker run

Для монтирования данных используются следующие параметры:

  • -v или --volume
  • --mount

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

Папки и файлы

Для примера - у меня есть следующая папка на хосте:

/home/alex/docker_data

В случае параметра "-v" указывается два пути "откуда:куда". В случае "--mount" это именованные параметры разделенные запятыми. Пример работы обоих:

-v /home/alex/docker_data:/usr/share/nginx/html
# или
--mount type=bind,source=/home/alex/docker_data,destination=/usr/share/nginx/html

В mount мы используем следующие параметры:

  • type - со значением 'bind' говорит, что мы монтируем папку или файл;
  • source - источник т.е. папка или файл, который мы хотим подключить к контейнеру;
  • destination - папка или файл внутри контейнера.

В обоих случаях мы можем монтировать данный доступные только для чтения (read-only) добавив "ro" в конце:

-v /home/alex/docker_data:/usr/share/nginx/html:ro
--mount type=bind,source=/home/alex/docker_data,destination=/usr/share/nginx/html,ro

Так выглядит запуск контейнера с проброшенной папкой:

docker run -d --name nginx_vol1 -v /home/alex/docker_data:/usr/share/nginx/html:ro nginx
# или
docker run -d --name nginx_vol2 --mount type=bind,source=/home/alex/docker_data,destination=/usr/share/nginx/html,ro nginx

Вы можете проверить работу смонтированной папки создав файл на хосте с последующим выводом внутри контейнера:

# тестовый файл
touch /home/alex/docker_data/testfile

# проверяем, что он виден внутри контейнеров
docker exec nginx_vol1 ls /usr/share/nginx/html
docker exec nginx_vol2 ls /usr/share/nginx/html

Проверка работы смонтированной папки в Docker

Подключение volume

При монтировании тома нужно учитывать следующие моменты:

  • название тома указывается без слешей;
  • если тома с этим названием нет, то он будет создан;
  • в случае с mount, в параметре type, указывается volume.

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

docker run -d --name nginx_vol1 -v docker_volume:/usr/share/nginx/html nginx
# или
docker run -d --name nginx_vol2 --mount type=volume,source=docker_volume,destination=/usr/share/nginx/html nginx

Так же как и с папками мы можем добавить ":ro" или ",ro" в конец значения, что бы дать права только на чтение директорий.

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

# создаем файл в одном контейнере
docker exec nginx_vol1 touch /usr/share/nginx/html/file1
# проверяем файл через другой контейнер
docker exec nginx_vol2 ls /usr/share/nginx/html

Проверка работы смонтированного именного тома в Docker

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

Вложенные тома и папки

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

  • "/usr/src/app" - папка с приложением, которое разрабатывает один или несколько разработчиков;
  • "/usr/src/app/node_modules" - содержит модули, которые компилируются под определенную систему.

Сложность с "node_modules" в следующем:

  • так как некоторые модули компилируются - они могут быть связаны с конкретной ОС и компилятором. Ошибки, в случае запуска на другой ОС, могут быть непредсказуемы;
  • папка создается долго, имеет большой объем и множество файлов;
  • папка может быть использована несколькими контейнерами.

Мы можем положить "node_modules" в том, что улучшит организацию. В то же время, папка "app", обновляется через GIT, который редко используется в контейнерах.

Один из оптимальных способов решения этих проблем является проброс "app" как папки, а "node_modules" как тома. Для начала мы создаем том и устанавливаем в него модули примерно так:

docker run -v $(pwd)/app/package.json:/usr/src/app/package.json \
           -v node_modules:/usr/src/app/node_modules \
           node \
           npm install

После того как том создан - мы можем использовать его с нашим приложением:

docker run -v $(pwd)/app:/usr/src/app \
           -v node_modules:/usr/src/app/node_modules \
           node

Просмотр привязанных томов

Что бы посмотреть тома уже в запущенном или остановленном контейнере - можно использовать команду 'docker inspect'. В следующем примере будут выведена только часть относящаяся к томам:

docker inspect nginx_vol2 --format "'{{json .Mounts}}'"

Просмотр томов, которые использует контейнер в Docker

Привязка томов из другого контейнера

С помощью параметра "--volumes-from" мы можем скопировать тома у запущенного или остановившегося тома. В значении мы указываем контейнер:

# контейнер 1
docker run -v $(pwd)/app:/usr/src/app \
           -v node_modules:/usr/src/app/node_modules \
           --name node1 \
           node

# контейнер 2
docker run --volumes-from node1 --name node2 node

Использование параметра volumes-from в Docker для копирования томов

Создание volume

Т.к. volume - это отдельны объект у docker есть команды, с помощью которых можно им управлять:

  • docker volume ls - выведет список томов;
  • docker volume inspect - покажет подробную информацию о томе в т.ч. его расположение на хосте;
  • docker volume create - создание нового тома;
  • docker volume prune - удалит все тома, которые не используются контейнерами;
  • docker volume rm - удалит один том.

Для примера создадим том, выведем все существующие и посмотрим детальную информацию о нем:

docker volume create some_nginx
docker volume ls
docker volume inspect some_nginx

Создание тома Docker

Можно легко не заметить как тома начнут занимать много места на диске. Что бы удалить тома, которые не смонтированы - можно использовать следующую команду:

docker volume prune

Удаление томов Docker

Параметр '-f' сделает то же самое, но без подтверждения.

Драйвера и options

В скриншоте выше можно было увидеть значения "Driver: local". Это значение говорит, что вы будете использовать функционал практически идентичным команде "mount" в Linux. Такой "mount" позволяет использовать nfs и cifs директории, а так же многие другие указывая их в опциях (параметры "-o" или "-opt").

Пример с nfs:

docker volume create --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.2.60,rw \
  --opt device=:/home/alex \
  nfs-volume

Создание NFS тома в Docker volume

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

Опции и драйвера напрямую используются редко. Мною лично только через другие приложения. Кроме этого они отличаются от ОС, которые вы используете. В Windows, например, опции не доступны по умолчанию.

Размещение томов в другой директории

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

В первом случае вы должны указывать местоположение тома при его создании. В примере ниже он будет храниться по пути "/home/alex/somevol":

docker volume create --driver local \
  --opt type=none \
  --opt device=/home/alex/somevol \
  --opt o=bind \
  home-vol2

Создание тома в другой директории в Docker

Пример смонтированного тома:

Создание тома в другой директории в Docker пример работы

Второй способ затрагивает не только "volume", но и все данные которые использует docker (образы, сеть, контейнеры и т.д.). Перед тем как начать - нужно остановить сервис docker:

sudo systemctl stop docker

После этого мы должны отредактировать или создать файл "daemon.json":

vi /etc/docker/daemon.json

Если у вас этого файла нет или он пустой, то содержимое должно быть следующим:

{
  "data-root": "путь до директории"
}

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

Данные с предыдущей директории так же нужно скопировать в новую директорию (в примере ниже это "/docker_data"):

sudo rsync -aP /var/lib/docker/ /docker_data

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

Можно запустить сервис и проверить, что все работает:

sudo systemctl start docker.service
docker volume create vol1
docker inspect vol1

Изменение директории работы Docker

 

Подключение тома и папки через Dockerfile

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

Создание тома будет иметь ряд ограничений:

  1. Вы не сможете указать имя тома или выбрать существующий. Имя будет сгенерировано автоматически;
  2. В любом случае том будет создан во время запуска контейнера т.е. так же как и в случае использования '-v';
  3. Каждое создание контейнера будет создавать новый том.

Для создания тома есть инструкция VOLUME. Пример синтаксиса:

FROM nginx
# указываем точку монтирования внутри контейнера
VOLUME /somedata1

Примерный результат где запускаются 2 контейнера с одного образа:

Создание и использование тома в образе Docker

Аналогичный результат можно получить используя одну из следующих команд:

docker run -v /somedata1 nginx
# или
docker run -v  $(docker volume create):/somedata1 nginx

Создание томов без названия в Docker

Если вы используете инструкцию "VOLUME" и параметр "-v" указывающий на одну и ту же директорию, то "-v" возьмет верх.

 

Volume в docker-compose

Docker-compose позволяет запускать несколько контейнеров используя один файл инструкций. Синтаксис монтирования томов может быть ограниченным и расширенным так же как "-v" и "--mount".

Для монтирования тома, кроме инструкции в самом контейнере, нужно указать дополнительную инструкцию 'volumes' в верхнем уровне. Для папки этого делать не нужно:

version: "3.8"
services:
  web:
    image: nginx:alpine
    volumes:
      # том
      - somevol:/app
      # папка
      - /home/alex:/app2
# для тома
volumes:
  somevol:

Том 'somevol' может использоваться совместно в нескольких контейнерах.

Если нам нужно дать права на том или папку, то мы просто добавляем 'ro' или 'rw' в коней пути:

...
    volumes:
      # том
      - somevol:/app:ro
      # папка
      - /home/alex:/app2:rw
...

Для монтирования так же есть расширенный синтаксис, похожий на команду mount в docker. Следующий пример аналогичен предыдущем по эффекту:

version: "3.8"
services:
  web:
    image: nginx:alpine
    volumes:
      # том
      - type: volume
        source: somevol
        target: /app1
      # папка
      - type: bind
        source: /home/alex
        target: /app2

volumes:
  somevol:

Есть еще инструкции, которые вы можете использовать. Ниже только их часть, но они используются редко:

    volumes:
      - type: volume
        source: somevol
        target: /app1
        # папка только для чтения
        read_only: true
        # не будет копировать файлы в том, которые уже находятся в контейнере
        volume:
           nocopy: true
      # папка
      - type: bind
        source: /home/alex
        target: /app2
        # папка только для чтения
        read_only: true
        # создаст папку на хосте если ее нет
        create_host_path: true
         

Как уже говорилось выше - мы можем использовать один и тот же том в нескольких контейнерах (сервисах). Кроме этого есть инструкция "volumes_from", которая использует тома с указанного контейнера. Ниже оба примера:

version: "3.8"
services:
  container1:
    image: nginx:alpine
    volumes:
      - somevol:/app1
      - /home/alex:/app2
  container2:
    image: nginx:alpine
    volumes:
      # тот же том, но в другом контейнере
      - somevol:/app2
  container3:
    image: nginx:alpine
    # берем тома из сервиса container1
    # с доступностью только на чтение
    volumes_from:
      - container1:ro
volumes:
  somevol:

Ниже результат работы таких инструкций. Как видно у контейнера 1 и контейнера 3 одни и те же тома:

Копирование и монтирование томов в docker compose

Если вам нужно удалить тома, которые были использованы или созданы при выполнении "docker compose up", можно добавить параметр "--volumes":

docker compose down --volumes

Удаление контейнеров с томами в docker compose

По умолчанию, в compose, тома используют приставку с названием проекта в названии. Если название тома "some_vol", а путь, в котором лежит файл docker-compose.yml следующий "/home/alex/project_name/", то том будет иметь название "project_name_some_vol".

Использование внешних томов

Если вам нужно использовать том, который был создан не в текущем файле docker-compose.yml, то вы можете его указать через параметр "external". Автоматический такой том не создается:

...
volumes:
  somevol:
    external: true

Монтирование внешних томов в docker compose

Создание тома в другой директории

Через compose мы так же можем указывать драйвера и опции. Так, например, мы создадим тома в другой директории по аналогии с тем, что делали выше:

...
volumes:
  my_test_volume:
    driver: local
    driver_opts:
       o: bind
       type: none
       device: /home/alex/compose_vol1

 

Tmpfs

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

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

Есть два способа создания tmpfs:

docker run \
  --tmpfs /app \
  nginx:latest
# или
docker run -d \
  --mount type=tmpfs,destination=/app,tmpfs-size=400,tmpfs-mode=1777 \
  nginx:latest

Удаление файлов в Docker tmpfs

При использовании параметра "--tmpfs" вы можете указать только директорию, которую планируете использовать.

При использовании "mount" у вас появляются не обязательные параметры:

  • tmpfs-size - размер в байтах. По умолчанию не ограничен;
  • tmpfs-mode - права на файлы. По умолчанию 1777. Можно не указывать специальные разрешения (т.е. 700, например).

Через Docker Compose мы так же можем создать и использовать tmpfs:

volumes:
  foo:
    driver: local
    driver_opts:
      type: "tmpfs"
      o: "o=size=100m,uid=1000"
      device: "tmpfs"

...