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 и не деплоить критические уязвимости.