Погружаемся в Docker: Dockerfile и коммуникация между контейнерами

    В прошлой статье мы рассказали, что такое Docker и как с его помощью можно обойти Vendor–lock. В этой статье мы поговорим о Dockerfile как о правильном способе подготовки образов для Docker. Также мы рассмотрим ситуацию, когда контейнерам нужно взаимодействовать друг с другом.


    В InfoboxCloud мы сделали готовый образ Ubuntu 14.04 с Docker. Не забудьте поставить галочку «Разрешить управление ядром ОС» при создании сервера, это требуется для работы Docker.

    Dockerfile


    Подход docker commit, описанный в предыдущей статье, не является рекомендованным для Docker. Его плюс состоит в том, что мы настраиваем контейнер практически так, как привыкли настраивать стандартный сервер.

    Вместо этого подхода мы рекомендуем использовать подход Dockerfile и команду docker build. Dockerfile использует обычный DSL с инструкциями для построения образов Docker. После этого выполняется команда docker build для построения нового образа с инструкциями в Dockerfile.

    Написание Dockerfile

    Давайте создадим простой образ с веб-сервером с помощью Dockerfile. Для начала создадим директорию и сам Dockerfile.
    mkdir static_web
    cd static_web
    touch Dockerfile
    

    Созданная директория — билд-окружение, в которой Docker вызывает контекст или строит контекст. Docker загрузит контекст в папке в процессе работы Docker–демона, когда будет запущена сборка образа. Таким образом будет возможно для Docker–демона получить доступ к любому коду, файлам или другим данным, которые вы захотите включить в образ.

    Добавим в Dockerfile информацию по построению образа:
    # Version: 0.0.1
    FROM ubuntu:14.04
    MAINTAINER Yuri Trukhin <trukhinyuri@infoboxcloud.com>
    RUN apt-get update
    RUN apt-get install -y nginx
    RUN echo 'Hi, I am in your container' \
            >/usr/share/nginx/html/index.html
    EXPOSE 80
    

    Dockerfile содержит набор инструкций с аргументами. Каждая инструкция пишется заглавными буквами (например FROM). Инструкции обрабатываются сверху вниз. Каждая инструкция добавляет новый слой в образ и коммитит изменения. Docker исполняет инструкции, следуя процессу:
    • Запуск контейнера из образа
    • Исполнение инструкции и внесение изменений в контейнер
    • Запуск эквивалента docker commit для записи изменений в новый слой образа
    • Запуск нового контейнера из нового образа
    • Исполнение следующей инструкции в файле и повторение шагов процесса.

    Это означает, что если исполнение Dockerfile остановится по какой-то причине (например инструкция не сможет завершиться), вы сможете использовать образ до этой стадии. Это очень полезно при отладке: вы можете запустить контейнер из образа интерактивно и узнать, почему инструкция не выполнилась, используя последний созданный образ.

    Также Dockerfile поддерживает комментарии. Любая строчка, начинающаяся с # означает комментарий.

    Первая инструкция в Dockerfile всегда должна быть FROM, указывающая, из какого образа нужно построить образ. В нашем примере мы строим образ из базового образа ubuntu версии 14:04.

    Далее мы указываем инструкцию MAINTAINER, сообщающую Docker автора образа и его email. Это полезно, чтобы пользователи образа могли связаться с автором при необходимости.

    Инструкция RUN исполняет команду в конкретном образе. В нашем примере с помощью ее мы обновляем APT репозитории и устанавливаем пакет с NGINX, затем создаем файл /usr/share/nginx/html/index.html.

    По-умолчанию инструкция RUN исполняется внутри оболочки с использованием обертки команд /bin/sh -c. Если вы запускаете инструкцию на платформе без оболочки или просто хотите выполнить инструкцию без оболочки, вы можете указать формат исполнения:
    RUN ["apt-get", "install", "-y", "nginx"]
    

    Мы используем этот формат для указания массива, содержащего команду для исполнения и параметры команды.

    Далее мы указываем инструкцию EXPOSE, которая говорит Docker, что приложение в контейнере должно использовать определенный порт в контейнере. Это не означает, что вы можете автоматически получать доступ к сервису, запущенному на порту контейнера (в нашем примере порт 80). По соображениям безопасности Docker не открывает порт автоматически, но ожидает, когда это сделает пользователь в команде docker run. Вы можете указать множество инструкций EXPOSE для указания, какие порты должны быть открыты. Также инструкция EXPOSE полезна для проброса портов между контейнерами.

    Строим образ из нашего файла

    docker build -t trukhinyuri/nginx ~/static_web
    

    , где trukhinyuri – название репозитория, где будет храниться образ, nginx – имя образа. Последний параметр — путь к папке с Dockerfile. Если вы не укажете название образа, он автоматически получит название latest. Также вы можете указать git репозиторий, где находится Dockerfile.
    docker build -t trukhinyuri/nginx \ git@github.com:trukhinyuri/docker-static_web
    

    В данном примере мы строим образ из Dockerfile, расположенном в корневой директории Docker.

    Если в корне билд контекста есть файл .dockerignore – он интерпретируется как список паттернов исключений.

    Что произойдет, если инструкция не исполнится?

    Давайте переименуем в Dockerfile nginx в ngin и посмотрим.



    Мы можем создать контейнер из предпоследнего шага с ID образа 066b799ea548
    docker run -i -t 066b799ea548 /bin/bash
    и отладить исполнение.

    По-умолчанию Docker кеширует каждый шаг и формируя кеш сборок. Чтобы отключить кеш, например для использования последнего apt-get update, используйте флаг --no-cache.
    docker build --no-cache -t trukhinyuri/nginx
    


    Использования кеша сборок для шаблонизации

    Используя кеш сборок можно строить образы из Dockerfile в форме простых шаблонов. Например шаблон для обновления APT-кеша в Ubuntu:
    FROM ubuntu:14.04
    MAINTAINER Yuri Trukhin <trukhinyuri@infoboxcloud.com>
    ENV REFRESHED_AT 2014–10–16
    RUN apt-get -qq update
    

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

    Инструкции Dockerfile

    Давайте рассмотрим и другие инструкции Dockerfile. Полный список можно посмотреть тут.

    CMD

    Инструкция CMD указывает, какую команду необходимо запустить, когда контейнер запущен. В отличие от команды RUN указанная команда исполняется не во время построения образа, а во время запуска контейнера.
    CMD ["/bin/bash", "-l"]
    

    В данном случае мы запускаем bash и передаем ему параметр в виде массива. Если мы задаем команду не в виде массива — она будет исполняться в /bin/sh -c. Важно помнить, что вы можете перегрузить команду CMD, используя docker run.

    ENTRYPOINT

    Часто команду CMD путают с ENTRYPOINT. Разница в том, что вы не можете перегружать ENTRYPOINT при запуске контейнера.
    ENTRYPOINT ["/usr/sbin/nginx"]
    

    При запуске контейнера параметры передаются команде, указанной в ENTRYPOINT.
    docker run -d trukhinyuri/static_web -g "daemon off"
    

    Можно комбинировать ENTRYPOINT и CMD.
    ENTRYPOINT ["/usr/sbin/nginx"]
    CMD ["-h"]
    

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

    WORKDIR

    С помощью WORKDIR можно установить рабочую директорию, откуда будут запускаться команды ENTRYPOINT и CMD.
    WORKDIR /opt/webapp/db
    RUN bundle install
    WORKDIR /opt/webapp
    ENTRYPOINT ["rackup"]
    

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

    USER

    Специфицирует пользователя, под которым должен быть запущен образ. Мы можем указать имя пользователя или UID и группу или GID.
    USER user
    USER user:group
    USER uid
    USER uid:gid
    USER user:gid
    USER uid:group
    

    Вы можете перегрузить эту команду, используя глаг -u при запуске контейнера. Если пользователь не указан, используется root по-умолчанию.

    VOLUME

    Инструкция VOLUME добавляет тома в образ. Том — папка в одном или более контейнерах или папка хоста, проброшенная через Union File System (UFS).
    Тома могут быть расшарены или повторно использованы между контейнерами. Это позволяет добавлять и изменять данные без коммита в образ.
    VOLUME ["/opt/project"]
    

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

    ADD

    Инструкция ADD добавляет файлы или папки из нашего билд-окружения в образ, что полезно например при установке приложения.
    ADD software.lic /opt/application/software.lic
    

    Источником может быть URL, имя файла или директория.
    ADD http://wordpress.org/latest.zip /root/wordpress.zip
    

    ADD latest.tar.gz /var/www/wordpress/
    

    В последнем примере архив tar.gz будет распакован в /var/www/wordpress. Если путь назначения не указан — будет использован полный путь включая директории.

    COPY

    Инструкция COPY отличается от ADD тем, что предназначена для копирования локальных файлов из билд-контекста и не поддерживает распаковки файлов:
    COPY conf.d/ /etc/apache2/
    


    ONBUILD

    Инструкция ONBUILD добавляет триггеры в образы. Триггер исполняется, когда образ используется как базовый для другого образа, например, когда исходный код, нужный для образа еще не доступен, но требует для работы конкретного окружения.
    ONBUILD ADD . /app/src
    ONBUILD RUN cd /app/src && make
    


    Коммуникация между контейнерами


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

    Проброс портов

    Такой способ связи уже был показан ранее. Посмотрим на варианты проброса портов чуть шире.
    Когда мы используем EXPOSE в Dockerfile или параметр -p номер_порта – порт контейнера привязывается к произвольному порту хоста. Посмотреть этот порт можно командой docker ps или docker port имя_контейнера номер_порта_в_контейнере. В момент создания образа мы можем не знать, какой порт будет свободен на машине в момент запуска контейнера.

    Указать, на какой конкретный порт хоста мы привяжем порт контейнера можно параметром docker run -p порт_хоста: порт_контейнера
    По-умолчанию порт используется на всех интерфейсах машины. Можно, например, привязать к localhost явно:
    docker run -p 127.0.0.1:80:80
    

    Можно привязать UDP порты, указав /udp:
    docker run -p 80:80/udp
    


    Линковка контейнеров

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

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

    Допустим у вас есть 2 контейнера: web и db. Чтобы создать связь, удалите контейнер web и пересоздайте с использованием команды --link name:alias.
    docker run -d -P --name web --link db:db trukhinyuri/webapp python app.py
    

    Используя docker -ps можно увидеть связанные контейнеры.

    Что на самом деле происходит при линковке? Создается контейнер, который предоставляет информацию о себе контейнеру-получателю. Это происходит двумя способами:
    • Через переменные окружения
    • Через /etc/hosts

    Переменные окружения можно посмотреть, выполнив команду env:
    $ sudo docker run --rm --name web2 --link db:db training/webapp env
        . . .
        DB_NAME=/web2/db
        DB_PORT=tcp://172.17.0.5:5432
        DB_PORT_5432_TCP=tcp://172.17.0.5:5432
        DB_PORT_5432_TCP_PROTO=tcp
        DB_PORT_5432_TCP_PORT=5432
        DB_PORT_5432_TCP_ADDR=172.17.0.5
    

    Префикс DB_ был взят из alias контейнера.

    Можно просто использовать информацию из hosts, например команда ping db (где db – alias) будет работать.

    Заключение


    В этой статье мы научились использовать Dockerfile и организовывать связь между контейнерами. Это только вершина айсберга, очень многое осталось за кадром и будет рассмотрено в будущем. Для дополнительного чтения рекомендуем книгу The Docker Book.

    Готовый образ с Docker доступен в облаке InfoboxCloud.

    В случае, если вы не можете задавать вопросы на Хабре, можно задать в Сообществе InfoboxCloud.
    Если вы обнаружили ошибку в статье, автор ее с удовольствием исправит. Пожалуйста напишите в ЛС или на почту о ней.

    Успешного использования Docker!
    Infobox 57,88
    Компания
    Поделиться публикацией
    Комментарии 23
    • 0
      Круто! А что технически делает галочка «Разрешить управление ядром ОС»?
      • +2
        У нас в облаке 2 типа виртуализации: контейнерная и гипервизорная. Галочка включает гипервизорную (VM) виртуализацию, которая требуется для работы Docker с подходящим ему ядром ОС. Также с этой опцией можно обновлять ядро ОС и ставить модули самостоятельно. С другой стороны контейнерная виртуализация эффективнее использует ресурсы оборудования.
        • 0
          Тогда не, тогда не круто. Вот если бы докер мог рулить контейнером в облаке, а так, скучно :)
          • +1
            Есть планы по работе Docker над OpenVZ/Cloud Server и по поддержке Docker на Windows Server, всему свое время.
      • 0
        А как вы решаете ситуации, когда нужно выстроить иерархию слинкованных контейнеров, а потом один из них перезапустить?
        Например database->back-end->api->ui и нужно пересобрать database.
        • 0
          В такой ситуации лучше использовать какой-нибудь оркестратор и service-discovery. А еще лучше — software defined network типа flannel или Kubernetes, который позволит все эти контейнеры разнести по разным хостам.
          Ну или посмотреть в сторону CoreOS (с тем же flannel).

          Сейчас у меня на продекшене это делается через skydock/skydns (т.е. service discovery), но никому не советую так делать.
          • –2
            Мой опыт пользования сабжем говорит о том, что он для решения таких задач не предназначен. Это просто пускалка сервисов в изолированном окружении. Хорошо подходит для тех, кому разделения процессов на уровне ядра — недостаточно или для софта, у которого конфликтующие зависимости.
            Есть конечно у докера привлекательные бизнес-возможности: можно гарантированно рабочий компонент системы быстренько запаковать и развернуть. Но поддерживать и отлаживать эти контейнеры — процедуры не всегда тривиальный и обычно не приносящие радости и удовольствия.
            Так и выходит — боссы радуются, админы недоумевают.
            имхо.
          • 0
            А у вас планируется поддержка CoreOS?
            • 0
              Да, но позже. Необходимо сделать качественную интеграцию с облаком. Пока CoreOS не готова к применению в Enterprise, хотя безусловно очень интересна.
            • 0
              Встречный вопрос хабровчанам: нужен шаблон CentOS 7 с Docker? В принципе не так важно, на чем запускать Docker контейнеры, но RHEL 7 уже официально поддерживает Docker. Нужен такой шаблон или Ubuntu с Docker достаточно? Если нужен, то зачем? На первый взгляд — т.к. у CentOS 7 ускорилась сетевая подсистема — было бы полезно. А вы как думаете? (если нужен — сделаем)
            • 0
              Не подскажите, как можно параметризовать докер-контейнер на этапе сборки?
              Н-р:
              — у меня в одном месте интернет через прокси и приходится его явно прописывать, ну н-р:
              RUN pip --proxy http://1.1.1.1:80/ install uwsgi

              — в другом прокси нет и команда выглядит:
              RUN pip install uwsgi


              при этом естественно хочется иметь один Docker файл для обоих случаев, нужно только уметь передавать туда параметр из cmd-line.
              • +3
                В таком случае лучше использовать какую-то утилиту для шаблонов, которая будет генерировать Dockerfile в зависимости от окружения.
                Т.е. будет Dockerfile.template
                {% if proxy %}
                RUN pip --proxy http://1.1.1.1:80/ install uwsgi
                {% else %}
                RUN pip install uwsgi
                {% endif %}
                

                И на этапе сборки сначала генерируется Dockerfile, а затем уже выполняется сборка образа.
                Почти такой подход у меня применяется для того, чтобы обновлять образы при деплое — подставляется хеш коммита в определенную строчку.

                Иметь один Dockerfile не получится из-за того, что он должен не зависить от окружения by design.

                Логичнее всего было бы сделать базовый образ для uwsgi, запушить в registry и использовать уже его.
                • 0
                  Соберите bash скрипт с вашими командами и конфиг который будет этот скрипт параметризовать. Складываете скрипты в ту же директорию что и докерфаил и когда вы выполняете
                  docker build .
                  

                  Ваш скрипт улетит серверу вместе с докерфаилом.

                  Вообще идеология докера предполагает что бы по изменению докерфаила всегда можно было отследить изменения контейнера, но прямо сейчас есть несколько проблем, которые не позволяют использовать этот подход. В итоге большинство базовых образов собраны с помощью дополнительный bash скриптов.
                • –1
                  Астрологи объявили неделю докера на хабре, количество постов о нем увеличено вдвое.

                  И знаете, это здорово!
                  • 0
                    Жаль, что конструкция
                    docker build git@github.com:somename/somerepo/somepath не работает
                    • +1
                      хочу заметить: поскольку каждая строка в Dockerfile — это отдельный коммит, то результирующий образ будет весить несколько больше, чем в случае если все команды писать в одну строку через "&&" или использовать скрипт установки и настройки, типа как в Vagrantfile
                      хочу спросить: у меня в роли хост-ОС Ubuntu 14.04. я пытался запустить CentOS контейнер. он запустился, все хорошо, но одно «но»: когда я пытался установить ПО (кажется, LAMP), из репозиториев он подтягивал мне пакеты для одной версии ядра, а при запуске искал среди установленных пакетов искал пакеты для другой версии ядра. на сколько я понял, то он использовал версию ядра хоста для установи, а потом пытался искать по той версии, которая записана у него где-то. я перелинковывал папки, но толку не вышло — все болотистей и болотистей получался результат. потому пришел к выводу, что в докере нужно использовать ту же ветвь дистрибутивов, что и на хосте. у меня на хосте убунта и в контейнерах корректно запускаются дистрибутивы убунты и дебиана. это правда или я что-то делал не так?
                      • 0
                        Все верно, докер использует ядро хоста.
                        • 0
                          У меня с хостами arch linux (3.16.4) и centos 6.5 (2.6.32) запуск и установка софта в контейнерах с busybox, ubuntu и centos разных версий проблем не вызывала. Почему у вас LAMP зависит от ядра? Мне такое кажется, как минимум, странным. Обычно такие вещи тянут зависимости не дальше libc.
                          • 0
                            будет свободная минутка — попробую воспроизвести ситуацию и отпишуть
                            • 0
                              скачал centos:centos6, установил там LAMP. кажется, все запустилось даже и не ругалось. не вспомню что я еще тогда устанавливал.
                              в любом случае, остаюсь на debian-контейнерах для веб-разработки — если работает, то не буду трогать
                              • 0
                                Если всё работает и устраивает, то можно и не париться. От добра добра не ищут.

                                У меня самая, пожалуй, неприятная вещь, связанная с докером — это ркн. При выполнении docker pull периодически попадаю на заблокированный ipшник cloudflare. При использовании в скриптах это крайне неудобно. А заворачивать трафик с серверов на cloudflare через vpn пока лень.

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

                          Самое читаемое