Construction


Название ENTRYPOINT всегда меня смущало. Это название подразумевает, что каждый контейнер должен иметь определенную инструкцию ENTRYPOINT. Но после прочтения официальной документации я понял, что это не соответствует действительности.


Факт 1: Требуется определить хотя бы одну инструкцию (ENTRYPOINT или CMD) (для запуска).


Если вы не определите ни одной из них, то получите сообщение об ошибке. Давайте попробуем запустить образ Alpine Linux, для которого не определены ни ENTRYPOINT, ни CMD.


$ docker run alpine
docker: Error response from daemon: No command specified.  
See 'docker run --help'.

Факт 2: Если во время выполнения определена только одна из инструкций, то и CMD и ENTRYPOINT будут иметь одинаковый эффект.


$ cat Dockerfile 
FROM alpine  
ENTRYPOINT ls /usr  

$ docker build -t test .

$ docker run test
bin  
lib  
local  
sbin  
share  

Мы получим те же результаты, если будем использовать CMD вместо ENTRYPOINT.


$ cat Dockerfile 
FROM alpine  
CMD ls /usr  # Using CMD instead  

$ docker build -t test .

$ docker run test
bin  
lib  
local  
sbin  
share  

Хотя этот пример и показывает, что между ENTRYPOINT и CMD нет никакой разницы, её можно увидеть, сравнив метаданные контейнеров.


Например, первый файл Dockerfile (с определенной ENTRYPOINT):


$ docker inspect b52 | jq .[0].Config
{
  ...
  "Cmd": null,
  ...
  "Entrypoint": [
    "/bin/sh",
    "-c",
    "ls /"
  ],
  ...
}

Факт 3: И для CMD, и для ENTRYPOINT существуют режимы shell и exec.


Из руководства:


ENTRYPOINT имеет два режима выполнения:
  • ENTRYPOINT ["executable", "param1", "param2"] (исполняемая форма, предпочтительно)
  • ENTRYPOINT command param1 param2 (форма оболочки)

До сих пор мы использовали режим shell, или оболочки. Это означает, что наша команда ls -l запускается внутри /bin/sh -c. Давайте попробуем оба режима и изучим запущенные процессы.


Режим "shell":


$ cat Dockerfile
FROM alpine  
ENTRYPOINT ping www.google.com  # "shell" format  

$ docker build -t test .

$ docker run -d test
11718250a9a24331fda9a782788ba315322fa879db311e7f8fbbd9905068f701  

Затем изучим процессы:


$ docker exec 117 ps
PID   USER     TIME   COMMAND  
    1 root       0:00 /bin/sh -c ping www.google.com
    7 root       0:00 ping www.google.com
    8 root       0:00 ps

Обратите внимание, что процесс sh -c имеет PID, равный 1. Теперь то же самое, используя режим "exec":


$ cat Dockerfile
FROM alpine  
ENTRYPOINT ["ping", "www.google.com"]  # "exec" format  

$ docker build -t test .

$ docker run -d test
1398bb37bb533f690402e47f84e43938897cbc69253ed86f0eadb6aee76db20d  

$ docker exec 139 ps
PID   USER     TIME   COMMAND  
    1 root       0:00 ping www.google.com
    7 root       0:00 ps

Мы видим, что при использовании режима exec команда ping www.google.com работает с идентификатором процесса PID, равным 1, а процесс sh -c отсутствует. Имейте в виду, что приведенный выше пример работает точно так же, если использовать CMD вместо ENTRYPOINT.


Факт 4: Режим exec является рекомендуемым.


Это связано с тем, что контейнеры задуманы так, чтобы содержать один процесс. Например, отправленные в контейнер сигналы перенаправляются процессу, запущенному внутри контейнера с идентификатором PID, равным 1. Очень познавательный опыт: чтобы проверить факт перенаправления, полезно запустить контейнер ping и попытаться нажать ctrl + c для остановки контейнера.


Контейнер, определенный с помощью режима exec, успешно завершает работу:


$ cat Dockerfile 
FROM alpine  
ENTRYPOINT ["ping", "www.google.com"]  

$ docker build -t test .

$ docker run test
PING www.google.com (172.217.7.164): 56 data bytes  
64 bytes from 172.217.7.164: seq=0 ttl=37 time=0.246 ms  
64 bytes from 172.217.7.164: seq=1 ttl=37 time=0.467 ms  
^C
--- www.google.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss  
round-trip min/avg/max = 0.246/0.344/0.467 ms  
$

При использовании режима "shell" контейнер работает не так, как ожидалось.


$ cat Dockerfile 
FROM alpine  
ENTRYPOINT ping www.google.com  

$ docker build -t test .

$ docker run test
PING www.google.com (172.217.7.164): 56 data bytes  
64 bytes from 172.217.7.164: seq=0 ttl=37 time=0.124 ms  
^C^C^C^C64 bytes from 172.217.7.164: seq=4 ttl=37 time=0.334 ms
64 bytes from 172.217.7.164: seq=5 ttl=37 time=0.400 ms  

Помогите, я не могу выйти! Сигнал SIGINT, который был направлен процессу sh, не будет перенаправлен в подпроцесс ping, и оболочка не завершит работу. Если по какой-то причине вы действительно хотите использовать режим shell, выходом из ситуации будет использовать exec для замены процесса оболочки процессом ping.


$ cat Dockerfile
FROM alpine  
ENTRYPOINT exec ping www.google.com  

Факт 5: Нет оболочки? Нет переменных окружения.


Проблема запуска НЕ в режиме оболочки заключается в том, что вы не можете воспользоваться преимуществами переменных среды (таких как $PATH) и прочими возможностями, которые предоставляет использование оболочки. В приведенном ниже файле Dockerfile присутствуют две проблемы:


$ cat Dockerfile 
FROM openjdk:8-jdk-alpine  
WORKDIR /data  
COPY *.jar /data  
CMD ["java", "-jar", "*.jar"]  # "exec" format  

Первая проблема: поскольку вы не можете воспользоваться переменной среды $PATH, нужно указать точное расположение исполняемого java-файла. Вторая проблема: символы подстановки интерпретируются самой оболочкой, поэтому строка *.jar не будет корректно обработана. После исправления этих проблем итоговый файл Dockerfile выглядит следующим образом:


FROM openjdk:8-jdk-alpine  
WORKDIR /data  
COPY *.jar /data  
CMD ["/usr/bin/java", "-jar", "spring.jar"]  

Факт 6: Аргументы CMD присоединяются к концу инструкции ENTRYPOINT… иногда.


Вот тут-то и начинается путаница. В руководстве есть таблица, цель которой – внести ясность в этот вопрос.


Entrypointscreen


Попытаюсь объяснить на пальцах.


Факт 6a: Если вы используете режим shell для ENTRYPOINT, CMD игнорируется.


$ cat Dockerfile 
FROM alpine  
ENTRYPOINT ls /usr  
CMD blah blah blah blah  

$ docker build -t test .

$ docker run test
bin  
lib  
local  
sbin  
share  

Строка blah blah blah blah была проигнорирована.


FACT 6b: При использовании режима exec для ENTRYPOINT аргументы CMD добавляются в конце.


$ cat Dockerfile
FROM alpine  
ENTRYPOINT ["ls", "/usr"]  
CMD ["/var"]  

$ docker build -t test .

$ docker run test
/usr:
bin  
lib  
local  
sbin  
share

/var:
cache  
empty  
lib  
local  
lock  
log  
opt  
run  
spool  
tmp  

Аргумент /var был добавлен к нашей инструкции ENTRYPOINT, что позволило эффективно запустить команду ls/usr/var.


Факт 6c: При использовании режима exec для инструкции ENTRYPOINT необходимо использовать режим exec и для инструкции CMD. Если этого не сделать, Docker попытается добавить sh -c в уже добавленные аргументы, что может привести к некоторым непредсказуемым результатам.


Факт 7: Инструкции ENTRYPOINT и CMD могут быть переопределены с помощью флагов командной строки.


Флаг --entrypoint может быть использован, чтобы переопределить инструкцию ENTRYPOINT:


docker run --entrypoint [my_entrypoint] test  

Все, что следует после названия образа в команде docker run, переопределяет инструкцию CMD:


docker run test [command 1] [arg1] [arg2]  

Все вышеперечисленные факты справедливы, но имейте в виду, что разработчики имеют возможность переопределять флаги в команде docker run. Из этого следует, что ...


Достаточно фактов… Что же делать мне?


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


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


Используйте ENTRYPOINT, если вы не хотите, чтобы разработчики изменяли исполняемый файл, который запускается при запуске контейнера. Вы можете представлять, что ваш контейнер – исполняемая оболочка. Хорошей стратегией будет определить стабильную комбинацию параметров и исполняемого файла как ENTRYPOINT. Для нее вы можете (не обязательно) указать аргументы CMD по умолчанию, доступные другим разработчикам для переопределения.


$ cat Dockerfile
FROM alpine  
ENTRYPOINT ["ping"]  
CMD ["www.google.com"]  

$ docker build -t test .

Запуск с параметрами по умолчанию:


$ docker run test
PING www.google.com (172.217.7.164): 56 data bytes  
64 bytes from 172.217.7.164: seq=0 ttl=37 time=0.306 ms

Переопределение CMD собственными параметрами:


$ docker run test www.yahoo.com
PING www.yahoo.com (98.139.183.24): 56 data bytes  
64 bytes from 98.139.183.24: seq=0 ttl=37 time=0.590 ms  

Используйте только CMD (без определения ENTRYPOINT), если требуется, чтобы разработчики могли легко переопределять исполняемый файл. Если точка входа определена, исполняемый файл все равно можно переопределить, используя флаг --entrypoint. Но для разработчиков будет гораздо удобнее добавлять желаемую команду в конце строки docker run.


$ cat Dockerfile
FROM alpine  
CMD ["ping", "www.google.com"]  

$ docker build -t test .

Ping – это хорошо, но давайте попробуем запустить контейнер с оболочкой вместо команды ping.


$ docker run -it test sh
/ # ps
PID   USER     TIME   COMMAND  
    1 root       0:00 sh
    7 root       0:00 ps
/ # 

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


Очистка


После запуска команд на хосте осталась куча остановленных контейнеров. Очистите их следующей командой:


$ docker system prune

Обратная связь


Буду рад услышать ваши мысли об этой статье ниже в комментариях. Кроме того, если вам известен более простой способ поиска в выдаче докера с помощью jq, чтобы можно было сделать что-то вроде docker inspect [id] | jq * .config, тоже напишите в комментариях.


Джон Закконе


Капитан Докера и инженер по облачным технологиям в IBM. Специализируется на Agile, микросервисах, контейнерах, автоматизации, REST, DevOps.


Ссылки:


  1. ENTRYPOINT vs CMD: Back to Basics
Поделиться с друзьями
-->

Комментарии (15)


  1. Crandel
    30.05.2017 10:06
    +3

    <img src=«https://habrastorage.org/web/ca2/564/592/ca25645927e9445c9a70475de1be7afc.png»" alt=«Entrypointscreen» />

    Опечатка


    1. olemskoi
      30.05.2017 11:51

      Спасибо! Исправил.


  1. grossws
    30.05.2017 16:03
    -4

    Вы статью вычитывали хоть минимально? Все эти Cat, Docker в шелле (хотя на mac'е из-за case-insensitive root fs будет работать), CMD [*java*, *-jar*, **.jar*], где " почему-то оказался заменён на *..


    1. grossws
      30.05.2017 23:57

      Сколько обиженных людей, ужас.


    1. olemskoi
      31.05.2017 12:03

      Спасибо за сообщение. Исправили косяки.


  1. ngalayko
    31.05.2017 10:01

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


    1. ggo
      31.05.2017 10:39

      Это если image пересобирать.
      А если у вас есть готовый image, и вам хочется немного поменять его поведение, то при правильном подходе, это можно сделать через CMD.


      1. ngalayko
        31.05.2017 10:41

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


        но не исключаю, что это знание как-нибудь пригодится :)


    1. Crandel
      31.05.2017 10:44

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


      1. ngalayko
        31.05.2017 10:47

        если нужно передать параметры в команду ENTRYPOINT это прекрасно делается так:
        Dockerfile:


        FROM ubuntu:latest
        
        ADD entrypoint.sh /
        RUN chmod +x /entrypoint.sh
        
        ENTRYPOINT ["/entrypoint.sh"]

        entrypoint.sh:


        #!/bin/sh
        
        echo "$@"

        а запуск:
        docker run -it image/name param1 param2


        1. Crandel
          31.05.2017 10:52

          Зачем один лишний файл, когда докер может сделать тоже самое? И в сабже очень хорошо это описали


          1. ngalayko
            31.05.2017 10:55

            best practice, описанная даже в доках
            https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#entrypoint


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


            1. Crandel
              31.05.2017 12:03

              The ENTRYPOINT instruction can also be used in combination with a helper script, allowing it to function in a similar way to the command above, even when starting the tool may require more than one step

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


              1. ngalayko
                31.05.2017 12:06

                Думаю мы оба согласны, что описанное тут в статье — частный случай описанного в доках. Так что если закладывать развитие, стоит применить более общее решение.


                1. Crandel
                  31.05.2017 12:17
                  +1

                  Согласен