Data Volume container.

В docker есть любопытный инструмент, который называется Data Volume сontainer. У меня он ассоциациируется с паттерном data transfer object.

Data Volume

Том (volume) - это внешний для контейнера каталог или файл, монтированный в контейнер. Есть три типа data volume:

  • Пользовательский каталог или файл, который монтируется с указанием полного пути в host-системе, пример: docker run -v "$(pwd)/log/:/log/" nginx;
  • Том под управлением docker с заданным именем;
  • Автоматически создаваемый том со служебным именем.

Второй тип получается, если имя каталога указать без пути. Пример: docker run -v test:/test/ nginx. В этом случае в /var/lib/docker/volumes/ будет создан каталог с указанным названием, или будет подключен существующий каталог. В Dockerfile директиве VOLUME можно указать имя каталога, который будет создан в /var/lib/docker/volumes/. Пример: VOLUME mysql_dump_v1.1:/docker-entrypoint-initdb.d/. При создании первого контейнеров из такого образа будет создан каталог /var/lib/docker/volumes/test, а при создании второго и следующих контейнеров, этот каталог будет для них общим. Такая вот любопытная недокументированная возможность.

Если не указывать имя каталога, который вы монтируете - будет создан новый каталог в /var/lib/docker/volumes/ с названием из 64 цифр в 16-ричной нотации, это третий тип. Пример команды: docker create -v /shared_folder nginx.

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

  • При удалении контейнера командой rm, как в docker rm drunk_ritchie, volume-каталоги не удаляются;
  • Если контейнер создан с параметром --rm, например, docker run --rm -v /var/lib/mysql mysql, созданные volume-каталоги будут удалены при завершении работы контейнера;
  • Если команде удаления указать параметр -v, например, docker rm -v drunk_ritchie, будут удалены автоматически созданные контейнеры, в том числе с заданным именем;
  • Если подключить том в несколько контейнеров, а затем удалять их с параметром -v, каталог будет удален из /var/lib/docker/volumes/ при удалении последнего контейнера, к которому подключен этот том. Если один из контейнеров, в которые был монтирован том, удален без -v, каталог тома останется, даже если следующий контейнер будет удален с параметром -v;
  • Пользовательские каталоги и файлы docker не удаляет.

Разобраться к чему относятся тома с автоматическими названиями в /var/lib/docker/volumes/ сложно, и при удалении контейнеров они остаются как мусор. В документации пишут, что разработчики работают над проблемой. В текущей реализации (1.8.2) я не вижу смысла использовать тома с автоматически генерируемыми именами. Самый удобный вариант - монтировать в контейнеры собственные, пользовательские каталоги. С файлами в них удобно работать, и удалить эти каталоги легко, когда станут не нужны.

Вот что еще нужно знать про тома в docker:

  • В каталог /var/lib/docker/volumes/ доступ есть только для рута, так что просмотреть содержимое автоматически созданных томов можно только через sudo;
  • Данные из томов не экспортируются при выполнении docker export, docker save и не сохранятся в образ командой docker commit;
  • Если при монтировании указать несуществующий каталог или если к нему нет доступа - docker не выдаст никакой ошибки;
  • При подключении в контейнер внешнего каталога у него остаются uid/gid host-системы;
  • В Dockerfile можно указать тома, которые будут созданы для контейнера, но только каталоги, файл можно подмонтировать только параметром команды run или create.

Перефразируя фильм "Адвокат дьявола", авторы docker дают нам тома в контейнерах, дарят этот экстраординарный подарок, а потом для своего ролика космических трюков устанавливает противоположные правила игры. Расшаривай каталог между контейнерами - но не удаляй. Монтируй свою папку - но не распространяй в образе. Записывай папки в образ и распространяй - но не расшаривай между контейнерами!

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

UID/GID и Data Volume

В контейнерах обычно есть свои пользователи и группы со своими uid/gid, а значения uid/gid у монтируемых каталогов присвоены в host-системе с ее пользователями. У группы docker, которая создатся в host-системе, gid равен 999. Однако, в официальных образах php и python пользователя с uid 999 нет вообще, а рабочие процессы исполняются под www-data 33:33. Кроме того, процессы в контейнерах с uid 0 могут спокойно поменять владельца подключаемого каталога, выставив выставив значение, отсутствующее в host-сиситеме. С большой вероятностью возникнут проблемы с правами доступа к подключаемым каталогам.

Надеюсь, когда-нибудь для образов docker примут соглашение, по которому во всех образах будет стандартная группа docker с одним gid. Пока что придется создать специальную группу для томов и добавить в нее своего пользователя.

$ sudo groupadd -g 56789 docker_volumes
$ sudo usermod -a -G docker_volumes gri

Подготовка образа с приложением

В документации предлагают создавать volume container из образа операционной системы или базы данных. Если такой контейнер экспортировать, в файл пойдет вся операционная система. Наследование от образа mysql добавит к размеру архива порядка 66 мегабайт при сжатии xz. Чтобы этого избежать, я создаю контейнер от Busybox - он позволяет выполнять простые команды и открывать терминал в контейнере, при этом добавляет в архив менее мегабайта.

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

Копирую скрипты приложения в каталоге source и готовлю образ со скриптами:

$ mkdir data_volume
$ chgrp docker data_volume
$ cd data_volume/
$ cp ~/app source
$ vi Dockerfile

Вот содержимое Dockerfile, из которого я создам образ со своим приложением:

FROM busybox
VOLUME /scripts
RUN adduser -D -u 56789 docker_volumes
COPY source /source/
USER docker_volumes
CMD test "$(ls -A "/scripts/" 2>/dev/null)" || cp /source/* /scripts/

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

Создаю образ и контейнер:

$ docker build -t grikdotnet/application .
$ cd ..
$ mkdir application
$ sudo chgrp docker_volumes application
$ docker run --name application -v "$(pwd)/application:/scripts" grikdotnet/application

Теперь у меня есть контейнер с data volume /scripts/, в который монтирован мой каталог ./application/, и в нем появилось мое приложение. Этот data volume я могу подмонтировать в контейнер php.

Этот образ удобно экспортировать

~$ docker save grikdotnet/application | xz > application.image.tar.xz
$ ls -lh application.dc.tar.xz
-rw-rw-r-- 1 gri gri 858K Sep  8 14:53 application.dc.tar.xz

и импортировать

$ docker rm application
$ docker rmi grikdotnet/application
$ rm -rf application/*
$ docker load --input application.image.tar.xz
$ docker run --name application -v "$(pwd)/application:/scripts" grikdotnet/application
$ ls -Al application
total 4
-rw-r--r-- 1 56789 docker_volumes 28 Sep  9 09:22 test.php

Да, пользователя с gid 56789 в host-системе нет.

UID/GID процессов среды исполнения

Конечно, PHP/Python/etc тоже может исполняться под пользователем docker_volumes.

В случае PHP для этого надо расширить официальный образ, используя Dockerfile, подобный такому:

FROM php:5.6-fpm
RUN adduser --group --system --uid 56789 --disabled-password docker_volumes
RUN apt-get update && apt-get install -y \
        libmcrypt-dev \
    && docker-php-ext-install mcrypt pdo_mysql
CMD ["php-fpm"]

и в подключаемом php-fpm.conf указать

user = docker_volumes
group = docker_volumes

Подключение приложения к контейнеру с PHP выполняется параметром --volumes-from.

$ docker run -d --name=php56 \
    --volumes-from application \
    my_php_image >>log/docker.php.log 2>&1

Пример пошаговых инструкций по запуску php можно найти в третьей части.