Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root

Рейтинг: 57.8% · 13 голосов
Практический курс по Docker: образы, контейнеры, тома, сети, Compose и продакшен. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
Marina_DevOps
Сообщения: 25
Зарегистрирован: 11 май 2026, 05:31

Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root

Сообщение Marina_DevOps »

АкадемияDocker с нуляГлава 13 из 17
Оглавление курса (17)
  1. Что такое Docker и какие задачи он решает
  2. Установка Docker и запуск первого контейнера
  3. Образы: слои, теги и реестр Docker Hub
  4. Пишем свой Dockerfile
  5. Тома и хранение данных: volumes и bind mounts
  6. Сети в Docker: связываем контейнеры между собой
  7. Переменные окружения и конфигурация контейнеров
  8. Docker Compose: поднимаем многоконтейнерное приложение
  9. Оптимизация образов: multi-stage сборка, размер и кэш слоёв
  10. Логи, отладка и мониторинг контейнеров
  11. Базовая безопасность контейнеров
  12. Подготовка к продакшену: что важно учесть
  13. Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root (вы здесь)
  14. Реестры образов: приватные registry, push и pull, теги и digest, imagePullSecrets
  15. BuildKit и buildx: multi-arch сборки, секреты сборки, экспорт кэша
  16. Docker в CI/CD: автосборка, сканирование образов (Trivy, Docker Scout), публикация
  17. Итоговый проект и куда расти: от Dockerfile до прода, обзор оркестрации (Kubernetes, Podman, OCI)
В главе 4 мы собрали первый Dockerfile, и он работал. Но между "работает у меня" и образом, который не стыдно катить в прод, лежит несколько директив, которые обычно учат уже на ошибках. Разберем их сейчас, до ошибок.

ENTRYPOINT и CMD, кто из них главный:

Обе директивы задают, что запустится при старте контейнера, и путаница между ними рождается из-за того, что по отдельности они похожи. Правило простое. CMD задает команду по умолчанию, которую целиком перетирают аргументы docker run. ENTRYPOINT задает несменяемую программу, к которой аргументы docker run дописываются в конец. В связке ENTRYPOINT это сама программа, а CMD ее аргументы по умолчанию.

Код: Выделить всё

FROM python:3.13-slim
WORKDIR /app
COPY app.py .
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8000"]
Теперь docker run myapp запустит python app.py --port 8000, а docker run myapp --port 9000 заменит только хвост. Подменить сам ENTRYPOINT можно флагом docker run --entrypoint, это удобно, когда нужно зайти в образ с шеллом и поковыряться.

У обеих директив две формы записи: exec в виде JSON-массива и shell обычной строкой. Для сервисов всегда используйте exec-форму. Shell-форма (ENTRYPOINT python app.py) оборачивает команду в /bin/sh -c, и PID 1 рискует оказаться шеллом, который SIGTERM от docker stop дочернему процессу не пересылает. Тогда каждый стоп висит 10 секунд и заканчивается SIGKILL, приложение не успевает корректно закрыть соединения и дописать данные. Справедливости ради, dash, bash и busybox sh умеют exec-оптимизацию: единственную простую команду без пайпов, && и редиректов они запускают через exec вместо форка, приложение само становится PID 1 и сигнал получает. Так что shell-форма нередко работает, но это поведение конкретного шелла, а не гарантия Docker: добавили пайп или вторую команду, и оптимизация исчезла. Exec-форма дает предсказуемый результат всегда, поэтому она и стандарт. Если процессу нужен полноценный init (зомби-процессы, форки), запускайте контейнер с docker run --init, демон подсунет tini как PID 1.

У exec-формы есть своя ловушка: она не проходит через шелл, значит никакой подстановки переменных. CMD ["python", "app.py", "--port", "$PORT"] передаст приложению буквальную строку $PORT, а не значение переменной. Варианты: читать переменную в самом приложении (os.environ), либо явно звать шелл, CMD ["sh", "-c", "python app.py --port $PORT"], помня про историю с PID 1 выше.

Производственный паттерн, который закрывает обе задачи сразу и используется в официальных образах postgres и nginx: ENTRYPOINT указывает на скрипт, который готовит окружение и в конце передает управление через exec "$@".

Код: Выделить всё

#!/bin/sh
set -e
# тут подстановка переменных, генерация конфига, ожидание базы
exec "$@"

Код: Выделить всё

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["python", "app.py", "--port", "8000"]
exec заменяет шелл на основной процесс без форка: тот становится PID 1, честно получает SIGTERM, а CMD остается легко переопределяемым хвостом. Откройте docker-entrypoint.sh официального postgres, в конце увидите ровно эту строку.

HEALTHCHECK, контейнер жив или только притворяется:

Запущенный процесс еще не значит работающий сервис. Приложение может висеть в дедлоке или потерять коннект к базе, а docker ps все равно покажет Up. HEALTHCHECK дает демону способ проверять это самостоятельно.

Код: Выделить всё

HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
  CMD wget -qO /dev/null http://127.0.0.1:8000/health || exit 1
Проверка обязана вернуть 0 (здоров) или 1 (болен). start-period дает приложению время подняться, провалы в этот период не засчитываются в retries. С Docker Engine 25 есть еще --start-interval, чтобы во время старта проверять чаще основного интервала. Статус виден в docker ps (healthy/unhealthy) и docker inspect. Учтите, чем проверка будет выполняться: в alpine wget есть из коробки (busybox), в debian-slim нет ни wget, ни curl. Либо ставьте явно, либо пишите проверку средствами самого приложения, для python подойдет python -c с urllib. Бывает и обратная ситуация: базовый образ уже объявил свой HEALTHCHECK, а вам он не нужен или мешает. Отключается строкой HEALTHCHECK NONE.

Два нюанса. Сам Docker нездоровый контейнер не перезапускает, статус используют другие: Compose через depends_on с condition: service_healthy, балансировщики, скрипты деплоя. А Kubernetes директиву HEALTHCHECK игнорирует полностью, там свои liveness и readiness пробы.

.dockerignore, что не должно попасть в сборку:

docker build отправляет демону контекст сборки, то есть содержимое каталога. Классический билдер слал его целиком, BuildKit (включен по умолчанию с Docker 23.0) передает контекст лениво и докачивает только нужное, но сути это не меняет: все, что не исключено, доступно сборке. Без .dockerignore в нее уезжают .git на сотни мегабайт, node_modules, дампы базы и, самое неприятное, .env с секретами, который через COPY . . оказывается в слое образа навсегда.

Код: Выделить всё

.git
node_modules
*.log
.env*
dump_*.sql
Dockerfile*
compose*.yml
Синтаксис похож на .gitignore. Если собираете один проект разными Dockerfile, BuildKit понимает персональные файлы вида Dockerfile.api.dockerignore.

Запуск не от root:

По умолчанию процесс в контейнере работает от root. Уязвимость в приложении превращается в root внутри контейнера, а с примонтированными каталогами хоста и в проблемы снаружи. Лечится директивой USER.

Код: Выделить всё

FROM node:22-alpine
RUN addgroup -S app && adduser -S -G app -u 10001 app
WORKDIR /app
COPY --chown=app:app . .
USER 10001
EXPOSE 8080
CMD ["node", "server.js"]
Фиксированный uid (10001) избавляет от сюрпризов с правами на volume. В самой директиве USER пишите число, а не имя: Kubernetes с политикой runAsNonRoot не умеет верифицировать имя пользователя из образа, kubelet видит просто строку и не запустит под, отдав CreateContainerConfigError, а USER 10001 проходит проверку без вопросов. COPY --chown ставит владельца сразу, отдельный RUN chown -R создал бы лишний слой размером со все файлы.

Про привилегированные порты. Сам Docker начиная с 20.10 выставляет в контейнерах sysctl net.ipv4.ip_unprivileged_port_start=0, поэтому непривилегированный процесс там спокойно слушает и 80, и 443. Но это любезность именно Docker: в Kubernetes и других рантаймах порты ниже 1024 без root по-прежнему требуют отдельной настройки (capability NET_BIND_SERVICE или тот же sysctl). Ради переносимости слушайте 8080 и пробрасывайте наружу как угодно. Многие официальные образы уже несут готового пользователя: в node это node, в postgres свой postgres.

Типичные грабли:

USER поставлен до RUN apt-get install, и сборка падает с permission denied. Все установки делайте от root, USER ближе к концу файла. HEALTHCHECK через curl в образе без curl: контейнер вечно unhealthy, хотя приложение в порядке. ENTRYPOINT ["sh", "-c", "..."] молча съедает аргументы docker run. $PORT в exec-форме уезжает в приложение буквальной строкой. Попытка удалить .env следующей строкой RUN rm не помогает, файл остается в предыдущем слое, спасает только .dockerignore.

Что в итоге:

Exec-форма и связка ENTRYPOINT плюс CMD, а для подготовки окружения entrypoint-скрипт с exec "$@", дают предсказуемый запуск и честную обработку сигналов. HEALTHCHECK превращает "процесс запущен" в "сервис отвечает". .dockerignore бережет секреты и время сборки, USER с числовым uid снимает целый класс рисков. В главе 14 займемся реестрами: поднимем приватный registry, разберем push, pull и чем тег отличается от digest.
👍4 ❤️1 🔥 😄 🤔2
✔ Лучший ответ сформирован автоматически — simon2100
Marina_DevOps писал(а):каждый стоп висит 10 секунд и заканчивается SIGKILL вот оно что. у нас gunicorn на каждом деплое убивался по таймауту, я полгода грешил на сам gunicorn и воркеры. оказалось shell-форма в ENTRYPOINT. переписал на массив, контейнер тушится за секунду. обидно даже
Перейти к ответу →
Аватара пользователя
simon2100
Сообщения: 2
Зарегистрирован: 11 май 2026, 20:04

Re: Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root

Сообщение simon2100 »

✔ Лучший ответ — сформирован автоматически
Marina_DevOps писал(а):каждый стоп висит 10 секунд и заканчивается SIGKILL
вот оно что. у нас gunicorn на каждом деплое убивался по таймауту, я полгода грешил на сам gunicorn и воркеры. оказалось shell-форма в ENTRYPOINT. переписал на массив, контейнер тушится за секунду. обидно даже
👍1 ❤️2 🔥 😄 🤔
Аватара пользователя
haskell_user
Сообщения: 2
Зарегистрирован: 18 май 2026, 09:10

Re: Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root

Сообщение haskell_user »

про healthcheck подтверждаю, час дебажил почему контейнер unhealthy. приложение живое, ручка /health из браузера отвечает. полез в docker inspect, а там exit code 127, в slim-образе тупо нет curl. так что совет про wget/busybox не для галочки
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
mrgiggles
Сообщения: 2
Зарегистрирован: 14 май 2026, 22:51

Re: Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root

Сообщение mrgiggles »

с USER есть засада на bind mount в разработке: контейнер пишет файлы от uid 10001, на хосте они становятся чужими и git начинает ругаться на permission denied. на linux лечу dev-сборкой с --build-arg UID=$(id -u), на маке docker desktop сам мапит владельца, там без разницы
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
ridleyjim1
Сообщения: 2
Зарегистрирован: 13 май 2026, 04:23

Re: Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root

Сообщение ridleyjim1 »

а можно в проде вообще без HEALTHCHECK, если docker все равно сам не рестартит? у нас просто restart: unless-stopped стоит и вроде живем
👍 ❤️ 🔥1 😄 🤔
Ответить
← Предыдущая глава
Подготовка к продакшену: что важно учесть
Следующая глава →
Реестры образов: приватные registry, push и pull, теги и digest, imagePullSecrets

Все главы курса «Docker с нуля»

Поделиться темой: ✈ Telegram VK

Вернуться в «Docker с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость