Оптимизация Docker-образов: multi-stage и не только
Образ на 1.2 ГБ и образ на 40 МБ делают одно и то же — но первый дольше грузится, дольше деплоится и занимает диск. Разберём, как ужать образ в разы без потери функциональности.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Почему размер образа важен
Большой образ — это медленный docker pull при каждом деплое, лишний трафик, забитый диск и медленный старт контейнеров. На CI это выливается в минуты ожидания на каждом коммите. Уменьшение образа ускоряет весь цикл разработки и экономит место на сервере.
Даже на быстрых NVMe-дисках MAATRIX аккуратный образ выигрывает: меньше слоёв — меньше операций чтения при запуске десятков контейнеров.
Разберёмся, из чего складывается размер. Образ состоит из слоёв: каждая инструкция FROM, RUN, COPY добавляет свой слой поверх предыдущего. Слои не удаляются — если вы установили пакет в одном слое, а удалили в следующем, файлы всё равно останутся в истории образа и займут место. Поэтому оптимизация — это и про выбор базового образа, и про то, как написаны сами инструкции.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США и РФ. Оплата картой РФ и по СБП.
Арендовать VPS для сборки образовMulti-stage сборка — главный приём
Идея проста: собираем приложение в одном «жирном» образе со всеми компиляторами, а в финальный образ копируем только готовый бинарник или артефакт. Пример для Go:
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=build /app /app
ENTRYPOINT ["/app"]
Первый stage весит под гигабайт со всем тулчейном Go, финальный — считанные мегабайты. В образ попадает только бинарник, а не исходники и компилятор.
Для статически слинкованного Go-бинарника можно пойти ещё дальше и использовать пустой образ scratch — в нём вообще нет операционной системы, только ваш файл. Размер падает до размера самого бинарника. Правда, тогда придётся вручную положить сертификаты и часовые пояса, если приложение ходит по HTTPS.
Тот же приём для Node.js
Для интерпретируемых языков в финал кладём только зависимости и код, без dev-пакетов и кэша npm:
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/index.js"]
Использование npm ci вместо npm install даёт воспроизводимую сборку по lock-файлу, а --omit=dev выкидывает инструменты разработки. Часто именно dev-зависимости — линтеры, тестовые фреймворки, сборщики — раздувают node_modules в разы, и в рантайме они не нужны.
.dockerignore и порядок слоёв
Каждая инструкция в Dockerfile — это слой, который кэшируется. Копируйте зависимости до исходников: пока package.json не меняется, слой с установкой берётся из кэша, и пересборка идёт за секунды.
Обязательно добавьте .dockerignore, чтобы в контекст сборки не улетали лишние гигабайты:
.git
node_modules
dist
*.log
.env
Dockerfile
Без этого файла COPY . . тащит в образ и .git, и локальные node_modules — образ пухнет на пустом месте.
Проверяем результат и чистим
Смотрим размеры образов и историю слоёв:
docker images
docker history myapp:latest
Регулярно убирайте мусор — висячие образы и кэш сборки съедают диск:
docker image prune -f
docker builder prune -f
- Не ставьте лишние пакеты — каждый apt/apk install остаётся в слое навсегда
- Объединяйте RUN и чистите кэш в той же инструкции:
apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* - Берите -alpine или -slim варианты базовых образов, где это возможно
Аккуратные образы особенно окупаются, когда вы деплоите часто: на VPS MAATRIX с ежедневными бэкапами лёгкий образ быстрее восстанавливается и разворачивается заново.
Подытожим правила хорошего Dockerfile: базовый образ — минимальный (alpine, slim, distroless или scratch); зависимости копируются и ставятся до исходного кода ради кэша; сборочные инструменты остаются в build-stage и не попадают в финал; временные файлы и кэш пакетных менеджеров удаляются в той же инструкции, где создаются; в контекст не летит ничего лишнего благодаря .dockerignore. Пять простых принципов, которые превращают гигабайтный образ в десятки мегабайт и ускоряют каждый деплой.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США и РФ. Оплата картой РФ и по СБП.
Арендовать VPS для сборки образовОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Насколько реально уменьшить образ?
Типично в 5-20 раз. Go-приложение с 900 МБ ужимается до 15-40 МБ, Node-сервис — с 1 ГБ до 150-200 МБ через alpine и multi-stage.
Alpine всегда лучший выбор?
Обычно да, но musl libc иногда ломает нативные модули. Если возникают странные ошибки — берите -slim на базе Debian.
Multi-stage замедляет сборку?
Нет, наоборот. Кэш слоёв и раздельные stage часто ускоряют пересборку, а финальный образ грузится быстрее.