Learning Platform
Глоссарий Troubleshooting
Урок 05.04 · 25 мин
Средний
DockerfileENTRYPOINTCMDmulti-stageOCIimage tagsdigest

Container images: Dockerfile, multi-stage, OCI

Pod в Kubernetes ссылается на образ строкой image: nginx:1.27 — за этой простой строкой стоит целая инфраструктура: формат хранения (OCI image spec), реестр (registry), теги, дайджесты, layers, build-инструменты. CKAD не требует от вас глубоких знаний Docker BuildKit или buildx, но требует понимания: как образ собирается, как переопределить ENTRYPOINT из Pod-spec, чем latest опасен в продакшене и зачем нужны multi-stage builds. Этот урок — про image side контракта между Dockerfile и Kubernetes.


Multi-stage builds: builder + runtime image

Dockerfile: инструкции в CKAD scope

Dockerfile — это текстовый файл с инструкциями, как собрать образ. Каждая инструкция создаёт новый layer (слой). Минимальный набор, который нужно знать:

# базовый образ — родитель
FROM node:20-alpine

# рабочая директория внутри контейнера, создастся если нет
WORKDIR /app

# build-time переменная (можно переопределить --build-arg)
ARG NODE_ENV=production

# environment variable — будет в контейнере при запуске
ENV NODE_ENV=${NODE_ENV} \
    PORT=8080

# копируем файлы из build context
COPY package*.json ./

# выполняем команду, результат сохраняется в новый layer
RUN npm ci --only=production

# копируем остальной код
COPY . .

# документация: указываем порт (НЕ открывает порт, см. ниже)
EXPOSE 8080

# непривилегированный пользователь — рекомендуется
USER 1000

# entrypoint: фиксированная команда
ENTRYPOINT ["node"]

# cmd: дефолтные аргументы для ENTRYPOINT
CMD ["server.js"]

Запомните различия:

ИнструкцияЧто делает
FROMУказывает родительский образ. Должен быть первым (или после ARG).
RUNВыполняет команду на момент build. Результат — новый layer.
COPYКопирует файлы из build context в образ. Создаёт layer.
ADDКак COPY, но умеет распаковывать .tar и качать по URL. Best practice — использовать COPY, если не нужно extra.
WORKDIRМеняет cwd для последующих инструкций и финального процесса.
ENVЗадаёт переменную окружения (доступна в RUN и в финальном процессе).
ARGBuild-time переменная. В runtime недоступна, если не сохранена через ENV.
EXPOSEДокументация о портах. Не публикует и не открывает.
ENTRYPOINTКоманда, которая запускается. Фиксированная.
CMDДефолтные аргументы для ENTRYPOINT. Легко переопределяемые.
USERUID/GID, под которым запускается процесс.
VOLUMEОбъявляет точку монтирования. В K8s обычно игнорируется в пользу spec.volumes.

ENTRYPOINT vs CMD, command vs args

Это любимая путаница на собеседованиях. Правило:

  • ENTRYPOINT — фиксированная часть команды (исполняемый файл).
  • CMD — дефолтные аргументы, которые легко переопределяются.

При запуске Docker берёт ENTRYPOINT + CMD и склеивает: получается одна командная строка.

ENTRYPOINT ["python"]
CMD ["app.py"]

→ контейнер запускает python app.py. При docker run myimage script.py получится python script.py (CMD заменён). При docker run --entrypoint sh myimage ENTRYPOINT тоже меняется.

В Kubernetes Pod-spec эти поля называются по-другому:

Маппинг Dockerfile ↔ Pod spec
Dockerfile ENTRYPOINTВ Pod spec поле называется containers[].command. Если задано — переопределяет ENTRYPOINT образа целиком.
=
spec.containers[].commandСписок строк, аналог exec form. Например: command: ['python']
Dockerfile CMDВ Pod spec поле называется containers[].args. Если задано — переопределяет CMD образа.
=
spec.containers[].argsСписок строк-аргументов. Например: args: ['app.py', '--debug']

Pod, который запускает python script.py --verbose поверх образа с ENTRYPOINT ["python"]:

spec:
  containers:
  - name: app
    image: myapp:1.0
    args: ["script.py", "--verbose"]   # переопределяет только CMD

Если хотим запустить полностью другую команду:

spec:
  containers:
  - name: app
    image: myapp:1.0
    command: ["/bin/sh"]                # переопределяет ENTRYPOINT
    args: ["-c", "echo hello; sleep 1000"]   # и CMD

exec form vs shell form

Инструкции RUN, CMD, ENTRYPOINT поддерживают две формы:

exec form (JSON array):

ENTRYPOINT ["nginx", "-g", "daemon off;"]

shell form (строка):

ENTRYPOINT nginx -g 'daemon off;'

Разница огромная. Shell form под капотом превращается в /bin/sh -c "nginx -g 'daemon off;'". Это означает:

  • PID 1 в контейнере — это /bin/sh, а не nginx.
  • SIGTERM от kubelet идёт в sh, а не в nginx. sh не пробрасывает сигнал дочернему процессу.
  • При kubectl delete pod контейнер получит SIGTERM, sh его проигнорирует, через 30 секунд kubelet пошлёт SIGKILL → 137. Никакого graceful shutdown.
WARNING

Используйте exec form (JSON array) для ENTRYPOINT и CMD, чтобы PID 1 был вашим процессом и получал сигналы напрямую. Shell form — частая причина «графsful shutdown не работает», особенно в legacy образах.

# хорошо
ENTRYPOINT ["node", "server.js"]

# плохо: PID 1 = sh, signals пропадут
ENTRYPOINT node server.js

Альтернатива — tini или dumb-init как PID 1, они корректно forward-ят сигналы:

RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

Multi-stage builds

Прямой Dockerfile для приложения с компилятором получится огромным: build tools, dev dependencies, исходники остаются в финальном образе. Решение — multi-stage build: несколько FROM в одном Dockerfile, каждый — отдельный stage. Финальный образ копирует только нужные артефакты из build stage.

# Stage 1: build
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build         # создаёт /app/dist

# Stage 2: runtime
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package*.json ./
USER 1000
ENTRYPOINT ["node", "dist/server.js"]

Что получаем:

  • Финальный образ — это node:20-alpine (~120 MB) + dist и production node_modules. Без npm, git, build-tools.
  • COPY --from=build берёт файлы из stage build, ничего из его слоёв не утекает в финальный.
  • Можно делать ещё уже: использовать distroless или scratch для финального stage.
# Финал на distroless: ни shell, ни package manager, минимум surface
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
WORKDIR /app
USER nonroot
ENTRYPOINT ["dist/server.js"]
TIP

Multi-stage даёт две выгоды: (1) меньший образ — быстрее pull, меньше storage в registry; (2) меньшая attack surface — нет компилятора, shell, sudo. Для production-сервисов distroless и multi-stage — золотой стандарт.


Layer cache

Каждая инструкция Dockerfile создаёт layer (read-only). Docker и BuildKit кэшируют слои по хэшу инструкции и контента: если инструкция и input не изменились — слой переиспользуется. Это даёт быстрые re-builds.

Порядок инструкций важен. Худший вариант:

FROM node:20
COPY . .                  # любое изменение в коде → invalidate cache ниже
RUN npm ci                # будет переустанавливать npm packages каждый билд

Правильно — копировать сначала dependency manifest, ставить зависимости, потом копировать код:

FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci                # этот слой кэшируется, пока package.json не изменится
COPY . .
RUN npm run build

OCI image format

«Docker image» исторически — но с 2017 стандартом стал OCI (Open Container Initiative). containerd, CRI-O, podman, kubelet — все работают с OCI образами. Docker — лишь один из инструментов сборки.

OCI image — это:

Структура OCI image
manifest.jsonJSON-документ, описывающий образ: список layers (digests), ссылка на config, архитектура (amd64/arm64), OS.
ссылается на
config.jsonОписание runtime: ENV, CMD, ENTRYPOINT, USER, WORKDIR, exposed ports. Это то, что Dockerfile инструкции компилируют.
layer #1 (tar.gz)Контент слоя: tar-архив изменений файловой системы относительно предыдущего слоя. Идентифицируется sha256-digest.
layer #2Следующий слой накладывается сверху через overlay filesystem.
layer #NВсе слои стакаются overlayfs в runtime, образуя финальную rootfs контейнера.

Registry (Docker Hub, GHCR, ECR, GCR, Harbor, registry.k8s.io) — это HTTP-сервер с API для push/pull OCI-артефактов. Когда kubelet делает pull, он:

  1. Резолвит имя registry.example.com/team/app:1.0 → URL.
  2. Качает manifest по тегу или digest.
  3. Параллельно качает layers по их digest (с проверкой sha256).
  4. Распаковывает в content store containerd.
  5. На запуске контейнера overlayfs стакает layers в rootfs.

Tags и digest

nginx:1.27 — это tag. Тэги в OCI mutable — владелец registry может переписать 1.27 так, чтобы он указывал на другой digest. latest — самый известный анти-паттерн: указывает на «последний пуш», содержимое меняется без вашего ведома.

nginx@sha256:abc123...def — это digest. Digest — это sha256 от manifest, immutable по определению: если digest указывает на этот образ — это всегда тот же самый набор байт.

Способ ссылкиПримерСтабильность
Просто имя (= :latest)nginxОпасно: latest меняется
Major tagnginx:1Изменится с выходом 1.28, 1.29
Minor tagnginx:1.27Изменится с выходом 1.27.1
Patch tagnginx:1.27.3Обычно стабилен, но технически mutable
Digestnginx@sha256:...Полностью immutable
WARNING

latest — anti-pattern в продакшене. Сегодня pull скачивает версию X, завтра pull скачивает Y (новый push в registry), и ваш Pod внезапно начал работать по-другому. Используйте конкретные теги (:1.27.3) или, для критичных production-нагрузок, digest pinning (@sha256:...).


Killer момент: digest pinning в production

Для compliance-критичных систем (банки, healthcare, supply-chain security) идеал — image:tag@digest:

containers:
- name: app
  image: registry.example.com/team/app:1.0.3@sha256:abc123...def

Это даёт:

  • Человекочитаемую версию (тег);
  • Криптографическую гарантию неизменности (digest);
  • Защиту от подмены registry или взлома namespace репозитория;
  • Возможность detection: если registry начнёт отдавать другой манифест по тегу — kubelet откажется деплоить.

Tools вроде crane, cosign, kustomize edit set image умеют автоматически разрешать tag в digest на момент CI.


Проверка знанийKnowledge check
В Dockerfile есть ENTRYPOINT ['python'] и CMD ['app.py']. Pod-spec задаёт args: ['debug.py']. Что запустится?
ОтветAnswer
python debug.py. args в Pod-spec переопределяет CMD из образа, но ENTRYPOINT остаётся. Чтобы переопределить ENTRYPOINT, нужно задать command.
Проверка знанийKnowledge check
Образ собран с ENTRYPOINT в shell-form (ENTRYPOINT node server.js). Pod нормально стартует. Что произойдёт при kubectl delete pod?
ОтветAnswer
Pod получит SIGTERM в PID 1 (/bin/sh -c). sh не пробросит сигнал дочернему процессу node. Через terminationGracePeriodSeconds (30s default) kubelet пошлёт SIGKILL, контейнер завершится с exit 137. Никакого graceful shutdown. Решение: использовать exec form ENTRYPOINT ['node', 'server.js'].
Проверка знанийKnowledge check
Зачем multi-stage build, если можно просто использовать alpine как FROM?
ОтветAnswer
Multi-stage позволяет иметь полноценный build environment (node, jdk, maven, gcc) в build stage и переносить только готовые артефакты в минимальный runtime stage. Финальный образ меньше (нет компилятора), быстрее pull, меньше attack surface. Просто alpine не даёт способа собрать приложение — для этого нужны build-tools, которых в alpine нет.
Проверка знанийKnowledge check
Production deploy использует image: myapp:latest. Pod долго работает стабильно. CI пушит новый latest. Что произойдёт с running Pod?
ОтветAnswer
Ничего не произойдёт сразу — running Pod продолжит работать со старым образом, который уже на узле. Но при следующем перезапуске контейнера (любая причина: OOM, restart, drain node) kubelet с imagePullPolicy: Always (по умолчанию для latest) скачает новый latest. Это значит: behavioural drift между Pod-ами при rolling restart, невозможность точно сказать какая версия где работает, проблема воспроизводимости инцидентов.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Dockerfile содержит ENTRYPOINT ["python"] и CMD ["app.py"]. Pod spec задаёт args: ["debug.py", "--verbose"]. Что запустится в контейнере?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5