Как уменьшить размер Docker-образа: практические способы
Практика3 минРедакция Innovialabs
Коротко
Главный выигрыш дают multi-stage сборка и лёгкий базовый образ (slim, alpine или distroless); затем очистите кеши пакетов в том же слое, добавьте .dockerignore и проверьте слои через dive.
Короткий ответ
Размер образа складывается из базового образа, зависимостей и всего, что осталось в слоях после сборки. Поэтому работают четыре вещи:
- Multi-stage build — собирать в одном образе, а в финальный копировать только результат.
- Лёгкая база —
slim,alpineилиdistrolessвместо полного дистрибутива. - Чистка в том же слое — кеши пакетных менеджеров удалять в той же инструкции
RUN, где они появились. - Анализ — смотреть, что реально лежит в слоях, утилитой
dive.
Маленький образ быстрее скачивается при деплое, быстрее стартует на новых нодах и содержит меньше пакетов, в которых могут найтись уязвимости.
Multi-stage build
Компиляторы, dev-зависимости и исходники нужны только на этапе сборки. В multi-stage сборке их можно оставить в промежуточной стадии:
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
Для Go и Rust эффект ещё сильнее: в финальный образ попадает один бинарник, и базой может быть distroless или даже scratch.
Как выбрать базовый образ
| База | Что внутри | Когда подходит |
|---|---|---|
Полный (debian, ubuntu) | Shell, пакетный менеджер, много утилит | Отладка, сложные системные зависимости |
slim | Урезанный Debian | Хороший компромисс для большинства языков |
alpine | musl libc, busybox, apk | Маленький размер, если зависимости совместимы с musl |
distroless | Только runtime, без shell и пакетного менеджера | Продакшен, минимальная поверхность атаки |
scratch | Пусто | Статически собранные бинарники |
Нюанс alpine: из-за musl вместо glibc некоторые нативные модули (например, Python-пакеты с C-расширениями) приходится собирать из исходников, и сборка становится дольше, а иногда и образ — больше. Если так происходит, slim часто оказывается практичнее.
Нюанс distroless: внутри нет shell, поэтому docker exec ... sh не сработает. Для отладки используют debug-варианты образов или эфемерные контейнеры.
Чистка кешей и слоёв
Каждая инструкция RUN, COPY и ADD создаёт слой. Файл, удалённый в следующем слое, всё равно остаётся в образе. Поэтому установка и очистка должны быть в одной команде:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
Аналогично:
pip install --no-cache-dir ...apk add --no-cache ...npm ciи затемnpm cache clean --forceв том жеRUN, если кеш попадает в образ.
Squashing слоёв (объединение в один) иногда уменьшает размер, но ломает переиспользование кеша слоёв между образами. Обычно multi-stage сборка решает ту же задачу аккуратнее.
.dockerignore
Без него в контекст сборки, а потом через COPY . . и в образ, попадают .git, node_modules, логи, локальные .env и тестовые данные. Минимальный пример:
.git
node_modules
*.log
.env
coverage
Это заодно ускоряет сборку и защищает от случайной утечки секретов в образ.
Как найти, что занимает место
docker images— общий размер образа.docker history <image>— размер каждого слоя и команда, которая его создала.- dive — интерактивно показывает содержимое каждого слоя, добавленные и изменённые файлы и «потерянное» место. Его можно запускать и в CI, чтобы образ не разрастался незаметно.
Частые ошибки
- Копировать весь проект до установки зависимостей — ломается кеш слоёв, и каждая сборка ставит всё заново.
- Оставлять dev-зависимости и инструменты сборки в финальном образе.
- Удалять файлы отдельной инструкцией
RUNи считать, что образ уменьшился. - Переходить на alpine вслепую, не проверив нативные зависимости.