Docker Compose: command та entrypoint, у чому різниця
ENTRYPOINT запускає програму, а command передає їй аргументи. Перевірте 4 комбінації перевизначення в Compose і чому entrypoint очищає CMD образу.
Docker Compose: command і entrypoint за одним правилом
У Docker Compose entrypoint: задає програму, яка запускається, а command: — аргументи, передані цій програмі. Процес контейнера складається зі списку entrypoint, до кінця якого додається список command. Уся інша поведінка, описана на цій сторінці, випливає з цього правила.
Ці два ключі відповідають двом інструкціям Dockerfile. entrypoint: замінює ENTRYPOINT образу. command: замінює CMD образу. Вони не є незалежними, і саме тут часто виникають проблеми: встановлення entrypoint: також відкидає CMD образу. У специфікації Compose це зазначено безпосередньо. Якщо entrypoint має значення, відмінне від null, Compose ігнорує команду за замовчуванням з образу.
Прочитайте, що вже оголошує image
Перш ніж щось перевизначати, перевірте, що постачається разом з image.
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 і зрештою виконує аргументи, які йому передали. Уся суть рішення полягає в тому, яку частину потрібно змінити. Щоб передати database flag, замініть 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, а також відмінності Compose
Dockerfile підтримує два синтаксиси. CMD ["nginx", "-g", "daemon off;"] — це формат exec: бінарний файл запускається безпосередньо, без shell. CMD nginx -g "daemon off;" — це формат shell: Docker перетворює його на /bin/sh -c 'nginx -g "daemon off;"', тому спочатку запускається shell, а ваша програма стає його дочірнім процесом.
Compose не застосовує це правило, і це часто дивує користувачів. Рядок у command: розбирається на аргументи та виконується безпосередньо, без оболонки /bin/sh -c. У документації Compose це прямо зазначено: поле command не виконується в контексті SHELL, визначеному в образі. Тому, якщо вам потрібні можливості shell, його потрібно викликати явно.
Саме тому command: echo "hello $$HOSTNAME" виводить буквальний текст hello $HOSTNAME. Жоден shell не обробив цей рядок, тому нічого не було розгорнуто. Якщо потрібен shell, викличте його явно:
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, у ролі PID 1 просто ігнорує SIGTERM. Він очікує весь період завершення, після чого його примусово вбивають. Через це обриваються відкриті з’єднання або незакомічені транзакції.
Shell перед вашою програмою підвищує ймовірність цієї проблеми, оскільки shell є PID 1, а більшість shell не пересилають сигнали дочірньому процесу. Деякі shell замінюють себе фінальною командою в рядку -c, тому іноді ваша програма все ж отримує PID 1. Це залежить від shell і точного рядка, тому не робіть припущень. Перевірте:
docker compose exec -T web cat /proc/1/cmdline | tr '\0' ' '; echoЯкщо замість вашої програми PID 1 має значення /bin/sh -c ..., є два способи виправити це. Використайте exec-форму в image або залиште shell і передайте йому процес за допомогою exec:
services:
web:
image: myapp:1.4
command: /bin/sh -c 'exec myapp --config /etc/myapp.toml'exec замінює процес shell вашою програмою, а не створює дочірній процес. Тому ваша програма отримує PID 1 і приймає сигнал.
Деякі програми створюють дочірні процеси й ніколи не збирають їхні статуси. Через це залишаються zombie-процеси, оскільки PID 1 також відповідає за їх збирання. Compose має для цього окремий перемикач:
services:
web:
image: myapp:1.4
init: true
stop_grace_period: 30sinit: true запускає невеликий init-процес як PID 1. Він пересилає сигнали вашому процесу та збирає дочірні процеси. stop_grace_period дає більше часу для справді повільного завершення. Якщо ваша програма очікує інший сигнал, stop_signal: SIGQUIT змінює сигнал, який надсилає Compose. Перевірте, який сигнал уже задано в image, за допомогою docker image inspect --format '{{.Config.StopSignal}}' nginx:1.27.
Якщо кожен сервіс у стеку docker compose down завжди завершується протягом десяти секунд, це означає, що SIGTERM ніхто не обробляє. Виправте це, перш ніж звинувачувати інструменти. Відмінності між docker compose down і stop та те, що видаляє кожна підкоманда, описано в розділі Відмінності між docker compose down і stop.
Та сама відмінність між exec-формою та shell-формою трапляється ще в одному місці. Healthcheck у формі test: ["CMD", "curl", "-f", "http://localhost/"] запускає binary безпосередньо, а test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] виконує його через shell, тому || має потрібне значення. Інші аспекти цього поля описано в розділі Як писати Compose healthcheck, які коректно повідомляють про помилки.
Додавання прапорця до офіційного образу
Саме за цим найчастіше приходять читачі. Вам потрібен ще один прапорець для 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 об’єднує override-файли, повністю замінюючи command, а не додаючи до нього значення. Тому другий файл, у якому також задано command:, непомітно перемагає.
Наведений вище ${POSTGRES_PASSWORD} розгортається Compose на хості з вашого файла .env, ще до створення контейнера. У розділі Файли середовища та секрети в Compose описано, де безпечно зберігати це значення.
Запуск одноразової міграції за допомогою docker compose run
docker compose run створює новий контейнер на основі того самого визначення сервісу та замінює команду на ту, яку ви вказали після імені сервісу. Точка входу образу також запускається, тому контейнер готується так само, як і контейнер довготривалого сервісу.
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, тому воно не конфліктує з контейнером сервісу.
Щоб також замінити точку входу, використайте такий прапорець:
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, в образі взагалі немає shell. Образи Distroless і образи на основі scratch часто не містять shell. Файлову систему все одно можна прочитати ззовні, не запускаючи entrypoint:
docker create --name probe myapp:1.4
docker export probe | tar -tv | head -40
docker rm probeЯкщо потрібно, щоб контейнер залишався запущеним і до нього можна було багаторазово підключатися, запустіть у ньому процес, який ніколи не завершується. Додайте це у файл перевизначень, який не слід комітувати:
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 command через оболонку?
Ні. На відміну від CMD у Dockerfile, рядок у Compose command: розділяється на аргументи та виконується безпосередньо, без оболонки /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 для сервісу.