
Название 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… иногда.
Вот тут-то и начинается путаница. В руководстве есть таблица, цель которой – внести ясность в этот вопрос.

Попытаюсь объяснить на пальцах.
Факт 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.
Ссылки:
Комментарии (15)

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

ngalayko
31.05.2017 10:01Обычно, если разработчику нужно переопределить исполняемую команду, то изменения внутри контейнера не ограничиваются одной лишь командой.
Тогда создается Dockerfile, унаследованный от базового с необходимыми приготовлениями и новым ENTRYPOINT. При таком подходе разницы не будет.
ggo
31.05.2017 10:39Это если image пересобирать.
А если у вас есть готовый image, и вам хочется немного поменять его поведение, то при правильном подходе, это можно сделать через CMD.
ngalayko
31.05.2017 10:41ну да, просто кажется, случай с небольшим изменением поведения встречается реже, и менее удобен, чем готовый image с вашими требованиями
но не исключаю, что это знание как-нибудь пригодится :)

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

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
Crandel
31.05.2017 10:52Зачем один лишний файл, когда докер может сделать тоже самое? И в сабже очень хорошо это описали

ngalayko
31.05.2017 10:55best practice, описанная даже в доках
https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#entrypoint
в примере, что я привел это неочевидно, но представьте, что логика запуска сложнее чем
echoи требует (на самом деле почти всегда) некоторых приготовлений не на этапе сборки, а сразу после запуска контейнера
Crandel
31.05.2017 12:03The 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
Тут описан немного другой случай, если для запуска процесса нужны дополнительные действия, не параметры
Crandel
Опечатка
olemskoi
Спасибо! Исправил.