Разница между command и entrypoint в Docker Compose
Узнайте, как правильно использовать command и entrypoint в docker-compose.yml. Разбираем четыре комбинации переопределения и почему установка entrypoint очищает CMD.
Разница между command и entrypoint в Docker Compose: единое правило
В Docker Compose entrypoint: задает программу, которая будет запущена, а command: определяет аргументы, передаваемые этой программе. Процесс контейнера формируется путем добавления списка command к списку entrypoint. Все остальные особенности поведения, описанные на этой странице, вытекают из этого утверждения.
Эти два ключа соответствуют двум инструкциям Dockerfile. entrypoint: заменяет ENTRYPOINT образа. command: заменяет CMD образа. Они не являются независимыми, и именно здесь часто возникают сложности: установка entrypoint: также отменяет CMD образа. Спецификация Compose прямо указывает на это. Если entrypoint не является пустым, Compose игнорирует любую команду по умолчанию, заданную в образе.
Изучение параметров образа
Прежде чем переопределять настройки, изучите, что именно поставляется в составе образа.
docker image inspect --format '{{json .Config.Entrypoint}}' postgres:16
docker image inspect --format '{{json .Config.Cmd}}' postgres:16Вы получаете ["docker-entrypoint.sh"] и ["postgres"], поэтому контейнер запускает docker-entrypoint.sh postgres. Этот скрипт при первом запуске создает каталог данных, считывает переменные POSTGRES_*, понижает привилегии до пользователя postgres и в конечном итоге выполняет переданные ему аргументы. Ключевой момент — понять, какую именно часть процесса вы хотите изменить. Чтобы передать флаг базе данных, замените command:. Если вы замените entrypoint:, то вся процедура инициализации не будет выполнена вовсе.
Четыре комбинации, показанные на миниатюрном изображении
Создайте образ, единственная задача которого — вывести список аргументов, с которыми он был запущен.
FROM alpine:3.20
ENTRYPOINT ["/bin/echo", "ep"]
CMD ["cmd"]docker build -t argdemo .services:
demo:
image: argdemoЗапускайте docker compose up после каждого изменения и читайте единственную строку, которую он записывает в лог.
- Ни один ключ не задан. Процесс —
/bin/echo ep cmd, а в логе отображаетсяep cmd. - Только
command: ["cmd2"]. Процесс —/bin/echo ep cmd2. Точка входа (entrypoint) остается нетронутой, изменяются только аргументы. - Только
entrypoint: ["/bin/echo", "ep2"]. Процесс —/bin/echo ep2, а в логе отображаетсяep2.cmdиз образа исчезает, и система не выдает никаких предупреждений. - Оба ключа заданы. Процесс —
/bin/echo ep2 cmd2. Это единственный случай, когда вы полностью контролируете список аргументов.
Почему установка entrypoint очищает CMD образа
CMD образа записывается как список аргументов по умолчанию для ENTRYPOINT этого образа. Если вы заменяете entrypoint, эти аргументы начинают относиться к программе, которая больше не запущена, поэтому Compose отбрасывает их вместо того, чтобы формировать командную строку, которую автор образа не планировал. docker run --entrypoint ведет себя так же, поэтому это поведение Docker, а не особенность Compose.
Последствия этого вполне конкретны. nginx:1.27 объявляет ENTRYPOINT ["/docker-entrypoint.sh"] и CMD ["nginx", "-g", "daemon off;"]. Установите entrypoint: /custom-init.sh, и ваш скрипт запустится с пустым списком аргументов. Скрипт, который заканчивается стандартным exec "$@", в таком случае не имеет ничего для выполнения, поэтому exec ничего не делает, скрипт доходит до последней строки, и контейнер завершает работу с кодом 0 без каких-либо сообщений об ошибках. Добавьте аргументы обратно самостоятельно:
services:
web:
image: nginx:1.27
entrypoint: /custom-init.sh
command: ["nginx", "-g", "daemon off;"]Правило, которое нужно запомнить: всякий раз, когда вы устанавливаете entrypoint:, сразу решайте, каким должен быть command:.
Exec-форма и shell-форма: различия в Docker и Compose
Dockerfile поддерживает два синтаксиса. CMD ["nginx", "-g", "daemon off;"] — это exec-форма: бинарный файл запускается напрямую, без участия оболочки. CMD nginx -g "daemon off;" — это shell-форма: Docker переписывает её как /bin/sh -c 'nginx -g "daemon off;"', поэтому сначала запускается оболочка, а ваша программа становится её дочерним процессом.
Compose не копирует это правило, что часто становится неожиданностью. Строка в command: разбивается на аргументы и выполняется напрямую, без обертки /bin/sh -c. Справочная документация Compose прямо указывает на это: поле command не выполняется в контексте SHELL, заданном в образе, поэтому при необходимости использования функций оболочки вы должны вызвать её самостоятельно.
Именно поэтому command: echo "hello $$HOSTNAME" выводит буквальный текст hello $HOSTNAME. Оболочка не обрабатывала эту строку, поэтому переменные не были раскрыты. Запрашивайте оболочку, если она вам нужна:
services:
demo:
image: alpine:3.20
command: /bin/sh -c 'echo "hello $$HOSTNAME"'Сигналы, PID 1 и корректное завершение docker compose down
docker compose stop и docker compose down отправляют SIGTERM процессу с PID 1 внутри каждого контейнера, ожидают stop_grace_period, а затем отправляют SIGKILL. Стандартный период ожидания составляет 10 секунд.
PID 1 в Linux обладает особыми свойствами. Ядро не применяет стандартное действие сигнала к PID 1, поэтому процесс, не установивший обработчик SIGTERM, просто игнорирует SIGTERM при запуске в качестве PID 1. Он продолжает работу в течение всего периода ожидания, после чего принудительно завершается, что приводит к разрыву открытых соединений или потере незафиксированных транзакций.
Оболочка (shell) перед вашей программой повышает вероятность такой ситуации, так как оболочка становится PID 1, а большинство оболочек не пересылают сигналы дочерним процессам. Некоторые оболочки заменяют себя финальной командой в строке -c, поэтому иногда ваша программа всё же получает PID 1. Это зависит от типа оболочки и конкретной строки, поэтому не стоит гадать. Проверьте это:
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echoЕсли в качестве PID 1 отображается /bin/sh -c ..., а не ваша программа, есть два способа исправления. Используйте exec-форму в образе или оставьте оболочку и передайте управление процессом с помощью exec:
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec заменяет процесс оболочки вашей программой вместо создания дочернего процесса, поэтому ваша программа получает PID 1 и принимает сигнал.
Некоторые программы порождают дочерние процессы и не собирают их, что приводит к появлению процессов-зомби, так как PID 1 также отвечает за сбор завершённых процессов (reaping). В Compose для этого есть специальный параметр:
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true запускает небольшой init-процесс в качестве PID 1, который пересылает сигналы вашему приложению и собирает дочерние процессы. stop_grace_period дает больше времени для медленного завершения работы. Если ваша программа ожидает другой сигнал, stop_signal: SIGQUIT позволяет изменить сигнал, который отправляет Compose. Проверьте, какой сигнал уже запрашивает образ, с помощью docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27.
Если стек, где docker compose down всегда занимает десять секунд на каждый сервис, сигнализирует о том, что ничто не обрабатывает SIGTERM. Исправьте это, прежде чем винить инструменты, и ознакомьтесь с разницей между docker compose down и stop, чтобы понять, что именно удаляет каждая из этих подкоманд.
Такое же разделение на exec и shell встречается еще в одном месте. Healthcheck, написанный как test: ["CMD", "curl", "-f", "http://localhost/"], запускает бинарный файл напрямую, тогда как test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] запускает его через оболочку, чтобы переменные вроде || имели значение. В статье Написание healthcheck для Compose, которые завершаются с ошибкой корректно рассматриваются остальные аспекты этой темы.
Добавление флага к официальному образу
Это именно то, зачем пришло большинство читателей. Вам нужен один дополнительный флаг для postgres, и при этом нельзя нарушить работу скрипта инициализации.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
command: postgres -c max_connections=200 -c shared_buffers=256MB
volumes:
pgdata:Изменился только command:, поэтому docker-entrypoint.sh по-прежнему выполняется и запускает то, что вы ему передали. Проверьте результат, вместо того чтобы полагаться на предположения:
docker compose up -d db
docker compose exec -T db psql -U postgres -c 'show max_connections;'В выводе должно отобразиться 200. Если там по-прежнему 100, выполните docker compose config и убедитесь, что ожидаемый вами command присутствует в объединенной конфигурации. Compose объединяет файлы переопределения, полностью заменяя command, а не добавляя к нему данные, поэтому второй файл, который также задает command:, молча перекроет первый.
Переменная ${POSTGRES_PASSWORD} выше раскрывается Compose на хосте из вашего файла .env до того, как контейнер будет создан. В разделе Файлы окружения и секреты в Compose описано, где безопасно хранить такие значения.
Запуск разовой миграции с помощью docker compose run
docker compose run создает новый контейнер на основе того же определения сервиса и заменяет команду на ту, которую вы вводите после имени сервиса. Entrypoint образа продолжает работать, поэтому контейнер подготавливается точно так же, как и постоянно работающий экземпляр.
docker compose run --rm app python manage.py migrate--rmудаляет контейнер после завершения выполнения команды. Без этого флага каждый запуск оставляет после себя остановленный контейнер, который будет виден вdocker compose ps -a.- Порты не публикуются. Контейнер
runигнорирует настройкиports:сервиса, если вы не добавите--service-ports, поэтому он не может конфликтовать с уже запущенным сервисом. - Зависимости запускаются первыми. Все, что указано в
depends_on, запускается перед вашей командой, а--no-depsпропускает этот этап. - Контейнер получает сгенерированное имя, например
myproject-app-run-9f2c1a, поэтому он никогда не конфликтует с контейнером сервиса.
Чтобы также заменить entrypoint, существует специальный флаг:
docker compose run --rm --entrypoint /bin/sh app -c 'python manage.py migrate'Результирующий список аргументов — /bin/sh -c 'python manage.py migrate', так как слова после имени сервиса по-прежнему считаются командой. docker compose exec — это другой инструмент, который работает иначе: он запускает процесс внутри уже работающего контейнера и полностью игнорирует как entrypoint:, так и command:. Используйте run для задачи, требующей свежего контейнера, и exec для проверки работающего. В шпаргалке по командам Compose остальные подкоманды приведены для сравнения.
Почему мой контейнер немедленно завершает работу?
Начните с кода завершения, так как он быстро сужает круг возможных причин.
docker compose ps -a
docker compose logs appКод завершения 0 и отсутствие вывода. Команда выполнилась и завершилась. Самая частая причина — переопределение entrypoint:, которое удалило CMD образа, из-за чего точка входа (entrypoint) запустилась с пустым списком аргументов и не получила данных для обработки.
Ошибка, заканчивающаяся на permission denied. У скрипта внутри образа нет бита исполнения. Обычно это происходит, если бит не был установлен для файла в репозитории. Установите его во время сборки с помощью COPY --chmod=0755 entrypoint.sh /entrypoint.sh.
Ошибка, заканчивающаяся на no such file or directory для файла, который вы видите в образе. У скрипта окончания строк в формате Windows. Его первая строка считывается как #!/bin/sh плюс байт возврата каретки, поэтому ядро ищет интерпретатор с этим байтом в имени и ничего не находит. Выполните dos2unix entrypoint.sh, а затем добавьте * text eol=lf в .gitattributes, чтобы проблема не повторилась.
executable file not found in $PATH. Бинарный файл, указанный в command:, отсутствует в образе, либо вы указали встроенную команду оболочки, например cd, там, где должна быть полноценная программа.
Получение доступа к оболочке (shell) в образе, у которого завершается с ошибкой entrypoint
Если entrypoint завершается до того, как вы успеваете что-либо проверить, замените его:
docker compose run --rm --entrypoint /bin/sh appЕсли эта команда возвращает executable file not found in $PATH, значит, в образе вообще нет оболочки. В образах Distroless и основанных на scratch она часто отсутствует. Вы всё равно можете прочитать файловую систему снаружи, не запуская entrypoint:
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probeЕсли вам нужно, чтобы контейнер оставался запущенным для многократного подключения, оставьте его работать на процессе, который никогда не завершается. Добавьте это в файл переопределения (override), который вы не будете фиксировать в репозитории:
services:
app:
entrypoint: ["tail", "-f", "/dev/null"]
command: []command: [] не является строго обязательным, так как установка entrypoint: уже очистила CMD образа, но явное указание фиксирует намерение для того, кто будет читать этот файл в будущем. Запустите контейнер и войдите в него:
docker compose -f compose.yaml -f compose.debug.yaml up -d app
docker compose exec app /bin/shТеперь запустите реальный entrypoint вручную и наблюдайте, на каком этапе он останавливается. Это позволит увидеть сообщение об ошибке прямо в терминале, а не в контейнере, который завершился полсекунды назад. Если вы только собираете свой первый стек, в разделе первый стек Compose на VPS описана структура файлов, на которую опирается всё вышесказанное.
FAQ
Почему мой контейнер завершается сразу после docker compose up?
Проверьте docker compose ps -a на наличие кода завершения. Код 0 без вывода обычно означает, что вы задали entrypoint: для сервиса, что также очистило CMD образа, поэтому entrypoint выполнился с пустым списком аргументов и завершился. Добавьте аргументы обратно с помощью command:. Ошибка, заканчивающаяся на permission denied, означает, что у скрипта entrypoint отсутствует бит исполнения. Ошибка, заканчивающаяся на no such file or directory для существующего файла, означает, что в скрипте используются символы переноса строки в формате Windows, из-за чего в строке shebang указывается несуществующий интерпретатор.
Удаляет ли установка entrypoint в Compose команду CMD из образа?
Да. Если entrypoint не является пустым, Compose игнорирует любую команду по умолчанию, объявленную в образе. Это задокументированное поведение, соответствующее docker run --entrypoint. Причина в том, что CMD образа записывается как аргументы для ENTRYPOINT этого же образа, поэтому после замены entrypoint старые аргументы перестают к чему-либо относиться. Установите command: в том же сервисе, если новому entrypoint требуются аргументы.
Выполняется ли строка в команде Compose через оболочку (shell)?
Нет. В отличие от CMD в Dockerfile, строка в command: Compose разбивается на аргументы и выполняется напрямую, без оболочки /bin/sh -c. Поэтому $VARIABLE никогда не раскрывается оболочкой внутри контейнера. Вызывайте оболочку самостоятельно, когда она нужна, как в command: /bin/sh -c 'echo "hello $$HOSTNAME"'. Двойной $$ экранирует знак доллара, чтобы Compose передал его в контейнер, а не раскрывал на хосте.
Почему docker compose down занимает десять секунд для одного контейнера?
Compose отправляет SIGTERM процессу с PID 1, ждет stop_grace_period (по умолчанию 10 секунд), а затем отправляет SIGKILL. Ядро не применяет стандартные действия по сигналу к PID 1, поэтому программа без обработчика SIGTERM игнорирует сигнал и всегда ждет истечения полного периода. Выясните, что именно является PID 1, с помощью docker compose exec -T app cat /proc/1/cmdline | tr '\0' ' '. Если это оболочка, переключите образ на exec-форму или напишите exec внутри строки оболочки. Если процесс порождает дочерние процессы, которые он не завершает, установите init: true для сервиса.