Перейти к содержанию

3. Образы Docker (глубоко)

Цель темы: уметь создавать маленькие, безопасные и быстрые образы: уверенно писать Dockerfile, понимать формирование слоёв и кэш сборки, применять best practices (multi-stage, базовые образы, .dockerignore) и иметь представление о формате OCI и multi-arch.


Определения терминов

Образ (image)

Образ — неизменяемый набор слоёв (файловая система + метаданные), на основе которого создаётся контейнер. Каждый слой описывается в манифесте образа; при запуске контейнера поверх слоёв образа добавляется слой для записи. Образ идентифицируется по репозиторию, тегу и digest (SHA-256).

Слой (layer)

Слой — результат одной или нескольких инструкций Dockerfile (чаще всего одна инструкция = один слой). Слои только для чтения; при сборке и при запуске контейнера они складываются через overlay. Кэш сборки привязан к слоям: если инструкция и контекст не изменились, слой берётся из кэша.

Dockerfile

Dockerfile — текстовый файл с инструкциями сборки образа. Каждая инструкция (FROM, RUN, COPY и т.д.) обычно создаёт новый слой. Порядок инструкций влияет на кэш: редко меняющиеся шаги лучше ставить выше, часто меняющиеся (например, копирование кода) — ниже.

Multi-stage build

Multi-stage build — несколько стадий (образов) в одном Dockerfile. Итоговый образ собирается из последней стадии; артефакты из предыдущих стадий копируются через COPY --from=. Позволяет не включать в финальный образ инструменты сборки (компилятор, node_modules для сборки) и уменьшить размер и поверхность атаки.

Distroless / Alpine

Alpine — минимальный базовый образ на базе musl libc и busybox; малый размер, но возможны отличия от glibc (совместимость, отладка). Distroless — образы без shell и пакетного менеджера, только приложение и минимальный runtime; очень маленькая поверхность атаки, отладка только через копирование бинарника наружу или через эпиhemeral контейнеры.


Dockerfile: основные инструкции

FROM

Задаёт базовый образ. Должна быть первой значимой инструкцией (до неё только ARG для динамического базового образа).

FROM node:20-alpine AS builder
FROM gcr.io/distroless/nodejs20-debian12

Рекомендуется указывать конкретный тег (например, 20-alpine, 1.2.3), а не latest, чтобы сборка была воспроизводимой.

RUN

Выполняет команду в слое образа. Каждая RUN — отдельный слой; объединение команд в одну уменьшает число слоёв.

RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

Практика

В Debian/Ubuntu очищайте кэш apt (rm -rf /var/lib/apt/lists/*) в той же RUN, чтобы слой не хранил лишние данные.

CMD и ENTRYPOINT

  • CMD — аргументы по умолчанию для запускаемого процесса. Можно переопределить при docker run.
  • ENTRYPOINT — фиксированная команда/исполняемый файл; аргументы из CMD или из docker run передаются ему как аргументы.

Форма «ENTRYPOINT + CMD»: ENTRYPOINT — программа, CMD — аргументы по умолчанию.

ENTRYPOINT ["/app/server"]
CMD ["--port", "8080"]

COPY и ADD

  • COPY — копирует файлы и каталоги из контекста сборки в образ. Не распаковывает архивы. Предпочтительнее для явности.
  • ADD — копирует и при необходимости распаковывает локальные tar-архивы; поддерживает URL (загрузка с удалённого сервера). Менее предсказуемо (кэш по URL, распаковка).

Рекомендация: использовать COPY; ADD — только если нужна распаковка tar из контекста.

COPY package*.json ./
COPY --chown=app:app ./src /app/src

ARG и ENV

  • ARG — переменная на время сборки. Не попадает в образ (если не использовать в ENV). Можно передать: docker build --build-arg NODE_ENV=production.
  • ENV — переменная в образе и в контейнере при запуске. Видна во время сборки и в runtime.
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
ENV NODE_ENV=production

USER

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

RUN addgroup --system app && adduser --system --ingroup app app
USER app
COPY --chown=app:app . /app

WORKDIR

Задаёт рабочую директорию для следующих инструкций и для CMD/ENTRYPOINT.

WORKDIR /app
COPY . .
RUN npm ci --omit=dev

HEALTHCHECK

Объявляет команду проверки здоровья контейнера. Docker (и оркестраторы) могут использовать результат для перезапуска или исключения из балансировки.

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

STOPSIGNAL

Сигнал, который отправляется процессу при остановке контейнера (по умолчанию SIGTERM). Полезно, если приложение ожидает другой сигнал.

STOPSIGNAL SIGQUIT

Слои и кэш сборки

Как формируются слои

Каждая инструкция (RUN, COPY, ADD и т.д.) создаёт новый слой. Слой кэшируется по хешу инструкции и хешу контекста (для COPY/ADD — хеш копируемых файлов). При изменении инструкции или контекста кэш инвалидируется для этого слоя и всех последующих.

Порядок инструкций

  • Ставить реже меняющиеся шаги выше: установка пакетов, копирование списков зависимостей (package.json, requirements.txt).
  • Часто меняющийся код приложения копировать ниже и по возможности отдельным слоем (сначала зависимости, потом код).

Пример: сначала COPY package*.json, RUN npm ci, затем COPY . . и сборка. Тогда при изменении только кода пересоберутся только последние слои.

Инвалидация кэша

  • Изменение содержимого файла, копируемого через COPY/ADD.
  • Изменение текста инструкции (включая пробелы в RUN).
  • Использование docker build --no-cache для полной пересборки.

Best practices

Multi-stage build

Сборка в одном образе (с компилятором, dev-зависимостями), копирование артефакта в чистый образ без инструментов сборки.

# stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# stage 2: runtime
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 8080
CMD ["node", "dist/index.js"]

Alpine vs distroless

  • Alpine: маленький образ, есть shell (удобно отлаживать), возможны отличия glibc/musl.
  • Distroless: почти нет shell и пакетов, минимальная поверхность атаки; отладка сложнее (нужен отдельный образ или kubectl debug).

Для production часто выбирают distroless для финального образа после multi-stage; для разработки — alpine или полный образ.

Закреплённые версии (pinned versions)

В FROM и при установке пакетов указывать конкретные версии (тег образа, версия пакета), а не latest. Это даёт воспроизводимые сборки и контроль обновлений.

.dockerignore

Исключать из контекста сборки ненужные файлы (тесты, документация, .git, node_modules, артефакты сборки). Это ускоряет передачу контекста и уменьшает риск случайно скопировать секреты или тяжёлые каталоги.

.git
node_modules
*.md
.env*
dist
coverage

Формат образа (OCI)

Образ состоит из:

  • Manifest — список слоёв (digest каждого), конфиг образа, аннотации; для multi-arch — manifest list (индекс по платформам).
  • Config — JSON с метаданными (архитектура, ОС, конфиг контейнера: CMD, ENV, entrypoint и т.д.).
  • Layers — tar-архивы с файловой системой каждого слоя.

Multi-arch: один тег (например, nginx:latest) может указывать на manifest list; при pull выбирается образ для нужной платформы (linux/amd64, linux/arm64). Сборка multi-arch: docker buildx build --platform linux/amd64,linux/arm64 -t myimage:tag --push .


Практика: наивный vs оптимизированный vs multi-stage

Naïve Dockerfile (плохо)

FROM node:latest
COPY . /app
WORKDIR /app
RUN npm install
CMD ["node", "index.js"]

Проблемы: тег latest, копирование всего до установки (плохой кэш), в образе dev-зависимости и исходники, нет отдельного пользователя.

Оптимизированный одностадийный

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN addgroup --system app && adduser --system --ingroup app app
USER app
EXPOSE 8080
CMD ["node", "index.js"]

Улучшения: фиксированный тег, кэш для зависимостей, только production-зависимости, непривилегированный пользователь.

Multi-stage

См. пример выше: стадия сборки (build) и стадия runtime только с артефактом и production-зависимостями. Размер итогового образа и поверхность атаки меньше; в образ не попадают компилятор и dev-инструменты.

Сравнение: замерить docker build время и docker images размер для одного и того же приложения при трёх подходах.


Паттерны использования

Паттерн Описание
Один слой для зависимостей, отдельно код COPY манифестов зависимостей → RUN установки → COPY кода. Максимальное использование кэша.
Multi-stage для бинарников и фронта Сборка в первом образе, копирование артефакта во второй; финальный образ без инструментов сборки.
.dockerignore и закреплённые версии Меньший контекст и воспроизводимость.
USER не root Снижение рисков при компрометации контейнера.
HEALTHCHECK для сервисов Корректная проверка готовности и живости в оркестраторах.

Антипаттерны

Антипаттерн Почему плохо Что делать
FROM latest Непредсказуемые обновления, невоспроизводимые сборки. Фиксировать тег (например, node:20-alpine).
COPY . . в начале Любое изменение кода ломает кэш для всех следующих слоёв. Копировать код после установки зависимостей.
Много RUN подряд Рост числа слоёв и размера образа. Объединять команды в одну RUN где уместно.
Секреты и dev-зависимости в образе Утечки и раздутый образ. Multi-stage, .dockerignore, секреты через runtime (env, mounts).
Запуск от root Увеличение риска при уязвимости. USER с непривилегированным пользователем.

Примеры из production

Медленная сборка в CI

Частая причина — большой контекст (без .dockerignore) и плохой порядок инструкций (кэш постоянно сбрасывается). Решение: .dockerignore, вынос зависимостей в отдельные слои, при возможности — кэш слоёв в registry (buildx, cache-from).

Большой размер образа

Образ раздут из-за dev-зависимостей, инструментов сборки, кэша пакетных менеджеров. Решение: multi-stage, npm ci --omit=dev / аналог, очистка кэша в той же RUN, выбор лёгкого базового образа (alpine, distroless).

Уязвимости в базовом образе

Регулярно обновлять базовый образ и пересобирать; сканировать образы (docker scout, trivy) в CI и не деплоить критические уязвимости.


Дополнительные материалы