Оптимизация образов: multi-stage сборка, размер и кэш слоёв

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

Оптимизация образов: multi-stage сборка, размер и кэш слоёв

Сообщение Marina_DevOps »

АкадемияDocker с нуляГлава 9 из 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 решает так много, и как multi-stage сборка режет размер образа в разы.

Как работает кэш слоев:

Из главы 3 вы помните, что каждая инструкция Dockerfile создает слой. При повторной сборке Docker сверяет инструкцию и ее входные файлы с прошлым результатом: если ничего не изменилось, слой берется из кэша. Но как только один слой инвалидировался, все слои ниже него пересобираются заново, кэш для них уже не действует.

Отсюда главное правило: редко меняющееся ставим выше, часто меняющееся ниже. Классическая ошибка: написать COPY . . до установки зависимостей. Тогда любая правка в коде инвалидирует копирование, и npm ci (или pip install, без разницы) отрабатывает с нуля при каждой сборке. Правильный порядок такой:

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

FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]
Зависимости теперь ставятся заново только при изменении package-lock.json, а правки кода затрагивают лишь последний COPY. На реальном проекте это превращает пятиминутную сборку в десятисекундную.

Второй обязательный шаг: файл .dockerignore в корне проекта.

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

node_modules
.git
dist
*.log
.env
Без него COPY . . утащит в образ локальный node_modules и каталог .git, который меняется при каждом коммите и стабильно ломает кэш. А попавший в образ .env с секретами это уже готовый инцидент, к этой теме вернемся в главе 11.

Multi-stage сборка:

Для сборки приложения нужны компилятор, dev-зависимости, весь тулчейн. Для запуска нужен только артефакт. Multi-stage позволяет собрать все в одном образе, а в финальный скопировать готовый результат:

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

FROM golang:1.24-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app ./cmd/app

FROM alpine:3.22
COPY --from=builder /bin/app /bin/app
USER nobody
ENTRYPOINT ["/bin/app"]
Образ golang:1.24-alpine весит больше 250 МБ, а финальный образ здесь получится около 15-20 МБ: alpine плюс статически слинкованный бинарник. В реестр уходит только последний stage, builder остается на машине сборки. Для Node схема та же: в builder ставим все зависимости и собираем фронтенд, в финальный stage копируем dist и только production-зависимости.

Проверить результат можно командой docker image ls, а docker history покажет размер каждого слоя, сразу видно, кто раздул образ.

Почему rm не уменьшает образ:

Слои только добавляют данные. Если файл создан в одном слое, а удален в следующем, физически он остается в образе. Поэтому мусор убирают в той же инструкции RUN, где он появился:

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

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*
Вынесете rm в отдельный RUN, и образ не похудеет ни на байт.

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

COPY . . первой строкой после FROM. Кэш зависимостей умирает, сборка всегда идет с нуля. Лечится порядком инструкций, как выше.

Забытый .dockerignore. Симптом: контекст сборки в сотни мегабайт (Docker пишет его размер в начале сборки) и кэш, который слетает без видимых причин.

Alpine не всегда друг. В нем musl вместо glibc, и нативные npm-модули или Python-пакеты иногда не собираются или ведут себя странно. Если уперлись, берите node:22-slim или python:3.13-slim: они на Debian, чуть тяжелее, зато без сюрпризов.

Нефиксированный тег базового образа. FROM node без тега или с тегом latest означает, что завтра сборка может пойти на другой версии. Указывайте хотя бы мажорную версию явно.

Если зависимостей много, посмотрите на кэш-монтирования BuildKit: строка RUN --mount=type=cache,target=/root/.npm npm ci сохраняет кэш пакетного менеджера между сборками, даже когда сам слой пересобирается.

Что усвоили:

Кэш слоев работает сверху вниз, поэтому порядок инструкций определяет скорость сборки. Multi-stage отделяет тулчейн от рантайма и сокращает образ в разы. Слои только добавляются, чистить надо в том же RUN, где намусорили. В следующей главе займемся уже запущенными контейнерами: логи, отладка и мониторинг.
👍2 ❤️6 🔥1 😄 🤔1
✔ Лучший ответ сформирован автоматически — q1pek
Marina_DevOps писал(а):Вынесете rm в отдельный RUN, и образ не похудеет ни на байт а если образ уже собран жирным, его можно как-то схлопнуть постфактум? видел упоминания docker build --squash, но у меня docker такой флаг не принимает. или единственный нормальный путь это переписать Dockerfile и пересобрать?
Перейти к ответу →
Аватара пользователя
q1pek
Сообщения: 1
Зарегистрирован: 02 июн 2026, 15:48

Re: Оптимизация образов: multi-stage сборка, размер и кэш слоёв

Сообщение q1pek »

✔ Лучший ответ — сформирован автоматически
Marina_DevOps писал(а):Вынесете rm в отдельный RUN, и образ не похудеет ни на байт
а если образ уже собран жирным, его можно как-то схлопнуть постфактум? видел упоминания docker build --squash, но у меня docker такой флаг не принимает. или единственный нормальный путь это переписать Dockerfile и пересобрать?
👍3 ❤️1 🔥 😄 🤔
Аватара пользователя
addict_yura
Сообщения: 3
Зарегистрирован: 30 май 2026, 06:57

Re: Оптимизация образов: multi-stage сборка, размер и кэш слоёв

Сообщение addict_yura »

а как multi-stage делать для питона? бинарника как в go нет, не копировать же site-packages руками. я сейчас в builder ставлю pip install в venv и копирую /opt/venv целиком во второй stage, работает, но не уверен что это правильный способ, а не костыль.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
fbird
Сообщения: 1
Зарегистрирован: 05 июн 2026, 08:48

Re: Оптимизация образов: multi-stage сборка, размер и кэш слоёв

Сообщение fbird »

спасибо, очень практичная глава. добавил multi-stage и .dockerignore, образ ужался с 1.4 гб до 230 мб. а после переноса COPY package.json выше npm ci сборка в gitlab ci упала с 7 минут до полутора. в docker history до этого даже не заглядывал, а там node_modules лежал двумя слоями, старый никуда не делся.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
moniealex
Сообщения: 1
Зарегистрирован: 27 май 2026, 14:28

Re: Оптимизация образов: multi-stage сборка, размер и кэш слоёв

Сообщение moniealex »

подтверждаю насчет alpine. у нас bcrypt на node-alpine отказался ставиться, пока не докинули python3 make g++ в builder, и это еще плюс 200 мб к стадии сборки. в итоге перешли на slim, разница в размере финального образа оказалась копеечная по сравнению с потраченными нервами.
👍1 ❤️1 🔥 😄 🤔1
Ответить
← Предыдущая глава
Docker Compose: поднимаем многоконтейнерное приложение
Следующая глава →
Логи, отладка и мониторинг контейнеров

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

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

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

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

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