RUN vs CMD vs Entrypoint in Docker

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

Давайте переименуем в 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

Введение


Существует много статей про запуск контейнеров и написание

docker-compose.yml

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

Вопрос этот стал актуальным, после того, как мы стали активно использовать docker-compose, вместо запуска отдельных докеров.

▍более сложный dockerfile

Хотя файл Dockerfile, который мы только что рассмотрели, получился аккуратным и понятным, он устроен слишком просто, в нём используется всего одна инструкция. Кроме того, там нет инструкций, вызываемых во время выполнения контейнера. Взглянем на ещё один файл, который собирает маленький образ. В нём имеются механизмы, определяющие команды, вызываемые во время выполнения контейнера.

▍ещё более сложный dockerfile

Рассмотрим ещё один файл Dockerfile, в котором будут использованы некоторые новые команды.

▍инструкция add

Инструкция

позволяет решать те же задачи, что и

COPY

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

В этом примере инструкция ADD была использована для копирования файла, доступного по URL, в директорию контейнера my_app_directory. Надо отметить, однако, что документация Docker не рекомендует использование подобных файлов, полученных по URL, так как удалить их нельзя, и так как они увеличивают размер образа.

Кроме того, документация предлагает везде, где это возможно, вместо инструкции ADD использовать инструкцию COPY для того, чтобы сделать файлы Dockerfile понятнее. Полагаю, команде разработчиков Docker стоило бы объединить ADD и COPY в одну инструкцию для того, чтобы тем, кто создаёт образы, не приходилось бы помнить слишком много инструкций.

Обратите внимание на то, что инструкция ADD содержит символ разрыва строки — . Такие символы используются для улучшения читабельности длинных команд путём разбиения их на несколько строк.

▍инструкция arg


Инструкция

позволяет задать переменную, значение которой можно передать из командной строки в образ во время его сборки. Значение для переменной по умолчанию можно представить в Dockerfile. Например:

ARG my_var=my_default_value

В отличие от ENV-переменных, ARG-переменные недоступны во время выполнения контейнера. Однако ARG-переменные можно использовать для задания значений по умолчанию для ENV-переменных из командной строки в процессе сборки образа.

▍инструкция copy

Инструкция COPY

Инструкция COPY представлена в нашем файле так: COPY . ./app. Она сообщает Docker о том, что нужно взять файлы и папки из локального контекста сборки и добавить их в текущую рабочую директорию образа. Если целевая директория не существует, эта инструкция её создаст.

▍инструкция entrypoint

Пункт перехода в какое-то место

Инструкция ENTRYPOINT позволяет задавать команду с аргументами, которая должна выполняться при запуске контейнера. Она похожа на команду CMD, но параметры, задаваемые в ENTRYPOINT, не перезаписываются в том случае, если контейнер запускают с параметрами командной строки.

Вместо этого аргументы командной строки, передаваемые в конструкции вида docker run my_image_name, добавляются к аргументам, задаваемым инструкцией ENTRYPOINT. Например, после выполнения команды вида docker run my_image bash аргумент bash добавится в конец списка аргументов, заданных с помощью ENTRYPOINT. Готовя Dockerfile, не забудьте об инструкции CMD или ENTRYPOINT.

В документации к Docker есть несколько рекомендаций, касающихся того, какую инструкцию, CMD или ENTRYPOINT, стоит выбрать в качестве инструмента для выполнения команд при запуске контейнера:

  • Если при каждом запуске контейнера нужно выполнять одну и ту же команду — используйте ENTRYPOINT.
  • Если контейнер будет использоваться в роли приложения — используйте ENTRYPOINT.
  • Если вы знаете, что при запуске контейнера вам понадобится передавать ему аргументы, которые могут перезаписывать аргументы, указанные в Dockerfile, используйте CMD.


В нашем примере использование инструкции

ENTRYPOINT ["python", "my_script.py", "my_var"]

приводит к тому, что контейнер, при запуске, запускает Python-скрипт

my_script.py

с аргументом

my_var

. Значение, представленное

my_var

, потом можно использовать в скрипте с помощью

. Обратите внимание на то, что в Dockerfile переменной

my_var

, до её использования, назначено значение по умолчанию с помощью

ARG

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

Документация Docker рекомендует использовать exec-форму ENTRYPOINT: ENTRYPOINT [“executable”, “param1”, “param2”].

▍инструкция env

Окружающая среда

▍инструкция expose

Инструкция EXPOSE

Инструкция EXPOSE указывает на то, какие порты планируется открыть для того, чтобы через них можно было бы связаться с работающим контейнером. Эта инструкция не открывает порты. Она, скорее, играет роль документации к образу, средством общения того, кто собирает образ, и того, кто запускает контейнер.

Для того чтобы открыть порт (или порты) и настроить перенаправление портов, нужно выполнить команду docker run с ключом -p. Если использовать ключ в виде -P (с заглавной буквой P), то открыты будут все порты, указанные в инструкции EXPOSE.

▍инструкция from


Файл Dockerfile должен начинаться с инструкции

FROM

, или с инструкции

ARG

, за которой идёт инструкция

FROM

Ключевое слово FROM сообщает Docker о том, чтобы при сборке образа использовался бы базовый образ, который соответствует предоставленному имени и тегу. Базовый образ, кроме того, ещё называют родительским образом.

В этом примере базовый образ хранится в репозитории ubuntu. Ubuntu — это название официального репозитория Docker, предоставляющего базовую версию популярной ОС семейства Linux, которая называется Ubuntu.

Обратите внимание на то, что рассматриваемый Dockerfile включает в себя тег 18.04, уточняющий то, какой именно базовый образ нам нужен. Именно этот образ и будет загружен при сборке нашего образа. Если тег в инструкцию не включён, тогда Docker исходит из предположения о том, что требуется самый свежий образ из репозитория. Для того чтобы яснее выразить свои намерения, автору Dockerfile рекомендуется указывать то, какой именно образ ему нужен.

:/>  Работа с обновлениями windows 10 очень долго 0 процентов

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

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

Структура контейнера (взято из документации)

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

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

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

▍инструкция run

Инструкция RUN

Инструкция RUN позволяет создать слой во время сборки образа. После её выполнения в образ добавляется новый слой, его состояние фиксируется. Инструкция RUN часто используется для установки в образы дополнительных пакетов. В предыдущем примере инструкция RUN apk update && apk upgrade сообщает Docker о том, что системе нужно обновить пакеты из базового образа.

То, что в командах выглядит как apk — это сокращение от Alpine Linux package manager (менеджер пакетов Alpine Linux). Если вы используете базовый образ какой-то другой ОС семейства Linux, тогда вам, например, при использовании Ubuntu, для установки пакетов может понадобиться команда вида RUN apt-get. Позже мы поговорим о других способах установки пакетов.

Инструкция RUN и схожие с ней инструкции — такие, как CMD и ENTRYPOINT, могут быть использованы либо в exec-форме, либо в shell-форме. Exec-форма использует синтаксис, напоминающий описание JSON-массива. Например, это может выглядеть так: RUN [“my_executable”, “my_first_param1”, “my_second_param2”].

В предыдущем примере мы использовали shell-форму инструкции RUN в таком виде: RUN apk update && apk upgrade && apk add bash.

Позже в нашем Dockerfile использована exec-форма инструкции RUN, в виде RUN [“mkdir”, “/a_directory”] для создания директории. При этом, используя инструкцию в такой форме, нужно помнить о необходимости оформления строк с помощью двойных кавычек, как это принято в формате JSON.

▍инструкция volume

Инструкция VOLUME

Инструкция VOLUME позволяет указать место, которое контейнер будет использовать для постоянного хранения файлов и для работы с такими файлами. Об этом мы ещё поговорим.

▍инструкция workdir

Рабочие директории

Инструкция WORKDIR позволяет изменить рабочую директорию контейнера. С этой директорией работают инструкции COPY, ADD, RUN, CMD и ENTRYPOINT, идущие за WORKDIR. Вот некоторые особенности, касающиеся этой инструкции:

  • Лучше устанавливать с помощью WORKDIR абсолютные пути к папкам, а не перемещаться по файловой системе с помощью команд cd в Dockerfile.
  • Инструкция WORKDIR автоматически создаёт директорию в том случае, если она не существует.
  • Можно использовать несколько инструкций WORKDIR. Если таким инструкциям предоставляются относительные пути, то каждая из них меняет текущую рабочую директорию.

Setup

To start, let’s create a script, log-event.sh. It simply adds one line to a file and then prints it:

Default arguments

This instruction can also be used to provide default arguments. For this use case, we don’t specify executable in this instruction at all, but simply define some arguments which are used as default/additional arguments for executable defined in the entrypoint instruction.

P.S: Anything defined in CMD can be overridden by passing arguments in docker run command.

Entrypoint and cmd with build arguments

This doesn’t work:

FROM alpine:3.7

# build argument with default value
ARG PING_HOST=localhost

# environment variable with same value
ENV PING_HOST=${PING_HOST}

# act as executable
ENTRYPOINT ["/bin/ping"]

# default command
CMD ["${PING_HOST}"]

It should be possible to build an image with build-arg and to start a container with an environment variable to override cmd as well.

docker build -t ping-image .
docker run -it --rm ping-image

Error: ping: bad address ‘${PING_HOST}’

UPDATE:

FROM alpine:3.7

# build argument with default value
ARG PING_HOST=localhost

# environment variable with same value
ENV PING_HOST ${PING_HOST}

COPY ./entrypoint.sh /usr/local/bin/

# act as executable
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

# default command
CMD $PING_HOST

entrypoint.sh

#!/bin/sh

/bin/ping $PING_HOST

This works because the entrypoint.sh enables variable substitution as expected.

How to override entrypoint using docker run

To illustrate how to override this command, we are going to run a container that echoes the message Hello World by combining ENTRYPOINT and CMD in the Dockerfile.

In the Dockerfile, the ENTRYPOINT command defines the executable, while CMD sets the default parameter.

FROM ubuntu
MAINTAINER sofija
RUN apt-get update
ENTRYPOINT [“echo”, “Hello”]
CMD [“World”]

If you build an image from this file and use it to run a Docker container, the output displays:

Docker ENTRYPOINT vs CMD instructions combined.

You can easily override the default CMD by adding the desired parameter to the docker run command:

sudo docker run [container_name] [new_parameter]

In the example below, we changed the CMD parameter World, by adding Sofija to the command. As a result, the output displays Hello Sofija.

Adding parameters to a docker run command to run a container with ENTRYPOINT and CMD instructions.

However, you may want to override the default executable and, for instance, run a shell inside a container. In that case, you need to use the --entrypoint flag and run the container using the following syntax:

sudo docker run --entrypoint [new_command] [docker_image] [optional:value]

To override the default echo message in our example and run the container interactively, we use the command:

sudo docker run -it --entrypoint /bin/bash [docker_image]

The output shows us we are now inside the container.

:/>  Не работает Панель задач - эффективные способы восстановления

Run Docker container by overriding entrypoint command and opening container shell.

Onbuild

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

ONBUILD ADD . /app/src
ONBUILD RUN cd /app/src && make

Shell form

When we try to execute an instruction using the shell form, normal shell processing takes place. It calls the command /bin/sh -c {command} behind the scenes.

It is of the form – <Your instruction> <the command>

Consider the commands below.

Volume

Инструкция VOLUME добавляет тома в образ. Том — папка в одном или более контейнерах или папка хоста, проброшенная через Union File System (UFS).

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

VOLUME ["/opt/project"]


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

Для чего это надо

Действительно, пусть приложение в контейнере B зависит от готовности сервиса в контейнере A. И вот при запуске, приложение в контейнере B этот сервис не получает. Что оно должно делать?

Варианта два:

  • первый — умереть (желательно с кодом ошибки)
  • второй — подождать, а потом всё равно умереть, если за отведённый тайм-аут приложение в контейнере B так и не ответило


После того как контейнер B умер,

docker-compose

(в зависимости от настройки конечно) перезапустит его и приложение в контейнере B снова попытается достучаться до сервиса в контейнере A.

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

Но, мы, в частности, столкнулись с ситуацией, когда контейнер А запускается и готовит данные для контейнера B. Приложение в контейнере B не умеет само проверять готовы данные или нет, оно сразу начинает с ними работать. Поэтому, сигнал о готовности данных нам приходится получать и обрабатывать самостоятельно.

Думаю, что можно ещё привести несколько вариантов использования. Но главное, надо точно понимать зачем вы этим занимаетесь. В противном случае, лучше пользоваться стандартными средствами docker-compose

Использование wait-for-it.sh

Сразу стоит сказать, что этот путь у нас не заработал так, как это описано в документации.


А именно, известно, что если в Dockerfile прописать

ENTRYPOINTCMD

, то при запуске контейнера будет выполняться команда из

ENTRYPOINT

, а в качестве параметров ей будет передано содержимое

CMD

Также известно, что ENTRYPOINT и CMD, указанные в Dockerfile, можно переопределить в docker-compose.yml

Формат запуска wait-for-it.sh следующий:

wait-for-it.sh адрес_и_порт -- команда_запускаемая_после_проверки

Тогда, как указано в

, мы можем определить новую

ENTRYPOINTdocker-compose.yml

, а

CMD

подставится из

Dockerfile

Итак, получаем:

Докер файл для контейнера А остаётся без изменений:

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

Используя кеш сборок можно строить образы из Dockerfile в форме простых шаблонов. Например шаблон для обновления APT-кеша в Ubuntu:

Как это реализуется

Для решения этой задачи мне сильно помогло описание

docker-compose

, вот

её часть

, рассказывающая про правильное использование

entrypointcmd

Итак, что нам нужно получить:

  • есть приложение А, которое мы завернули в контейнер А
  • оно запускается и начинает отвечать OK по порту 8000
  • а также, есть приложение B, которое мы стартуем из контейнера B, но оно должно начать работать не ранее, чем приложение А начнёт отвечать на запросы по 8000 порту

Официальная документация предлагает два пути для решения этой задачи.

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

Второй это использование уже написанного командного файла wait-for-it.sh.Мы попробовали оба пути.

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

Связь через сетевые порты — лишь один способ коммуникации. 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


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

mkdir static_web
cd static_web
touch Dockerfile

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

Добавим в Dockerfile информацию по построению образа:

Написание собственной entrypoint

Что такое

entrypoint

Это просто исполняемый файл, который вы указываете при создании контейнера в Dockerfile в поле ENTRYPOINT. Этот файл, как уже было сказано, выполняет проверки, а потом запускает основное приложение контейнера.

:/>  Как создать список файлов в папке

Итак, что у нас получается:

Создадим папку Entrypoint.

В ней две подпапки — container_A и container_B. В них будем создавать наши контейнеры.

Немного идеологии


Если внимательно читать документацию, то там всё написано. А именно — каждый

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

которыми он собирается работать, ему доступны.

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

Образы docker

Вспомните о том, что контейнер Docker — это образ Docker, вызванный к жизни. Это — самодостаточная операционная система, в которой имеется только самое необходимое и код приложения.

Образы Docker являются результатом процесса их сборки, а контейнеры Docker — это выполняющиеся образы. В самом сердце Docker находятся файлы Dockerfile. Подобные файлы сообщают Docker о том, как собирать образы, на основе которых создаются контейнеры.

Каждому образу Docker соответствует файл, который называется Dockerfile. Его имя записывается именно так — без расширения. При запуске команды docker build для создания нового образа подразумевается, что Dockerfile находится в текущей рабочей директории. Если этот файл находится в каком-то другом месте, его расположение можно указать с использованием флага -f.

Контейнеры, как мы выяснили в первом материале этой серии, состоят из слоёв. Каждый слой, кроме последнего, находящегося поверх всех остальных, предназначен только для чтения. Dockerfile сообщает системе Docker о том, какие слои и в каком порядке надо добавить в образ.

Каждый слой, на самом деле, это всего лишь файл, который описывает изменение состояния образа в сравнении с тем состоянием, в котором он пребывал после добавления предыдущего слоя. В Unix, кстати, практически всё что угодно — это файл.

Базовый образ — это то, что является исходным слоем (или слоями) создаваемого образа. Базовый образ ещё называют родительским образом.

Базовый образ — это то, с чего начинается образ Docker

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

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

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

Когда мы используем

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

Работающий вариант

Тогда, остаётся только один вариант. То, что у нас указано в

CMDDockerfile

, мы должны перенести в

commanddocker-compose.yml

Тогда, Dockerfile контейнера B оставим без изменений, а docker-compose.yml будет выглядеть так:

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

docker build -t trukhinyuri/nginx ~/static_web

, где trukhinyuri – название репозитория, где будет храниться образ, nginx – имя образа. Последний параметр — путь к папке с Dockerfile. Если вы не укажете название образа, он автоматически получит название latest. Также вы можете указать git репозиторий, где находится Dockerfile.

Файлы dockerfile

В файлах Dockerfile содержатся инструкции по созданию образа. С них, набранных заглавными буквами, начинаются строки этого файла. После инструкций идут их аргументы. Инструкции, при сборке образа, обрабатываются сверху вниз. Вот как это выглядит:

FROM ubuntu:18.04
COPY . /app


Слои в итоговом образе создают только инструкции

FROMRUNCOPY

, и

ADD

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

Здесь мы исходим из предположения, в соответствии с которым используется образ Docker, основанный на Unix-подобной ОС. Конечно, тут можно воспользоваться и образом, основанным на Windows, но использование Windows — это менее распространённая практика, работать с такими образами сложнее. В результате, если у вас есть такая возможность, пользуйтесь Unix.

Для начала приведём список инструкций Dockerfile с краткими комментариями.

Conclusion

In this article, we’ve seen the differences and similarities between the Docker instructions: run, cmd, and entrypoint. We’ve observed at what point they get invoked. Also, we’ve taken a look at their uses and how they work together.

Итоги

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

Заключение


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

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

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

Успешного использования Docker!

Оставьте комментарий