Минимальные образы: alpine, distroless, .dockerignore
Размер образа — это не косметика. От него зависит скорость деплоя, расходы на реестр и количество уязвимостей, которые вы тащите в продакшен вместе с лишними пакетами. В этом уроке разбираемся, как выбор базового образа и пара продвинутых приёмов уменьшают образ с сотен мегабайт до десятков.
Минимальный базовый образ — это образ, в котором есть ровно то, что нужно вашему приложению для работы в рантайме, и ничего сверх того. Принцип простой: всё, чего нет в образе, нельзя сломать, нельзя проэксплуатировать и не нужно скачивать при каждом деплое.
Зачем за это вообще бороться на практике? Во-первых, скорость: образ на 1 ГБ тянется при каждом pull на каждой ноде кластера, при каждом холодном старте раннера CI. Урезали до 80 МБ — деплой и автоскейлинг ускорились в разы. Во-вторых, поверхность атаки: каждый лишний пакет в образе — это потенциальный CVE. Полный debian или ubuntu тащит сотни системных библиотек, утилит и интерпретаторов, которые ваше приложение никогда не вызывает, но которые исправно светятся в отчётах сканеров безопасности. Меньше пакетов — меньше уязвимостей и меньше работы по их латанию.
Выбор базового образа
Главное решение принимается в первой строке Dockerfile — в инструкции FROM. Сравним четыре типовых варианта на примере Python-приложения.
alpine
Дистрибутив, построенный вокруг минимализма: базовый образ весит около 5 МБ. Вместо привычной библиотеки glibc здесь используется musl libc, а вместо apt — пакетный менеджер apk. Это даёт крошечный размер, но именно из-за musl возможны проблемы совместимости с пакетами, которые рассчитывают на glibc (об этом ниже в «частых ошибках»).
FROM python:3.12-alpine
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
slim (debian-slim)
Это обычный debian, из которого вырезали документацию, локали и редко нужные пакеты, но оставили родную glibc и менеджер apt. Золотая середина: образ заметно меньше полного debian (десятки МБ против сотен), но сохраняет полную бинарную совместимость. Если alpine что-то ломает — почти всегда спасает переход на slim.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
distroless
Образы от Google, в которых нет ничего лишнего: ни командной оболочки (shell), ни пакетного менеджера, ни даже ls и cat. Только сам рантайм (например, Python или JVM) и системные библиотеки. Это максимальная безопасность — взломщику, попавшему внутрь, буквально нечем работать, — но и максимальная сложность отладки: внутрь нельзя зайти через docker exec ... sh, потому что оболочки там просто нет. Обычно используется как финальный слой в multi-stage сборке.
FROM gcr.io/distroless/python3-debian12
WORKDIR /app
COPY . .
CMD ["app.py"]
| Образ | Типичный размер | Есть shell? | Пакетный менеджер | Когда брать |
| debian / ubuntu (полный) | 120–300+ МБ | да | apt | отладка, инструменты сборки |
| slim (debian-slim) | 50–120 МБ | да | apt | выбор по умолчанию, нужна glibc |
| alpine | 10–60 МБ | да (ash) | apk | чистые приложения без нативных зависимостей |
| distroless | 20–80 МБ | нет | нет | прод-рантайм, максимум безопасности |
.dockerignore
Файл .dockerignore вы уже встречали в азах — напомним одной фразой: он исключает файлы из контекста сборки, то есть из того набора файлов, который CLI отправляет демону Docker перед стартом сборки. Здесь это лишь один из приёмов минимизации, но важный.
.git
.gitignore
node_modules
__pycache__
*.pyc
.venv
Dockerfile
.dockerignore
*.md
tests/
.env
.vscode
Эффект двойной. Во-первых, контекст не раздувается: без этого файла команда docker build . упакует и отправит демону весь каталог целиком, включая .git и node_modules на сотни мегабайт — а это лишние секунды (или минуты) на каждую сборку. Во-вторых, ускоряется и удешевляется COPY . .: копируется меньше файлов, а главное — мусорные файлы не попадают внутрь образа. Бонусом вы не утащите в образ .env с секретами.
Объединение RUN и очистка
Классическая ошибка новичка — ставить пакеты «как в терминале», несколькими командами и без уборки за собой:
# ПЛОХО: лишние слои и мусор остаётся в образе
FROM debian:12-slim
RUN apt-get update
RUN apt-get install -y curl git
RUN apt-get install -y build-essential
Здесь три проблемы: каждый RUN создаёт отдельный слой; индекс пакетов из apt-get update остаётся внутри образа навсегда; кэш так и не очищен. Правильный вариант — одна инструкция RUN, склеенная через &&, с очисткой кэша в той же команде:
# ХОРОШО: один слой, кэш вычищен тут же
FROM debian:12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl git build-essential \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
Для разных экосистем есть свои флаги, отключающие кэш прямо на лету, чтобы не чистить его вручную:
# Alpine: --no-cache не сохраняет индекс пакетов
apk add --no-cache curl git
# Python: не складывать колёса в кэш pip
pip install --no-cache-dir -r requirements.txt
# Debian/Ubuntu: чистим список пакетов в том же слое
apt-get clean && rm -rf /var/lib/apt/lists/*
Как это работает под капотом
Чтобы понять, почему очистку нужно делать именно «в той же команде», вспомним устройство образа. Образ — это стопка неизменяемых слоёв; каждая инструкция RUN, COPY и ADD добавляет сверху новый слой, а итоговый размер образа равен сумме всех слоёв.
Ключевой момент: слой нельзя «похудеть» задним числом. Если в одном слое вы скачали 200 МБ кэша, а в следующем RUN удалили его командой rm, то верхний слой лишь запишет пометку «этих файлов больше нет». Но сами 200 МБ физически остаются в нижнем слое и продолжают занимать место в образе и в реестре. Файл удалён логически, но не исчез. Именно поэтому установка и очистка должны жить в одном RUN: тогда мусорные файлы вообще не попадают в финальный слой.
Отдельная техническая деталь про alpine — замена glibc на musl libc. Эти библиотеки реализуют стандарт C по-разному, и заранее скомпилированные бинарники, собранные под glibc, в alpine могут просто не запуститься. Поэтому в alpine такие пакеты нередко приходится собирать из исходников прямо при сборке образа — что съедает выигрыш по размеру и времени.
Частые ошибки
- Брать
alpine«по умолчанию». На нативных модулях (node-gyp, бинарные пакеты Python вродеnumpy/pandas, драйверы БД) из-заmuslсборка падает или тянет тяжёлый toolchain. Частоslimв итоге и меньше, и быстрее. - Чистить кэш в новом слое.
rm -rfв отдельной инструкцииRUNне уменьшает образ — удалённые файлы остаются в нижнем слое. Очистка обязана быть в том жеRUN, что и установка. - Брать
distrolessбез отладочного образа. Когда в проде что-то падает, без оболочки внутрь не зайти. Уdistrolessесть теги:debugсbusybox— держите их для диагностики, а в основном образе оставляйте «чистый» вариант. - Игнорировать
.dockerignore. Раздутый контекст сборки замедляет каждыйbuildи тащит в образ.git,node_modulesи секреты из.env.
Вывод: разница между подходами хорошо видна в выводе docker images для одного и того же приложения.
REPOSITORY TAG SIZE myapp debian-full 412MB myapp slim 95MB myapp alpine 58MB myapp distroless 52MB
Итоги
- Выбор образа начинается с
FROM:slim— разумный дефолт,alpine— для чистых приложений,distroless— для безопасного прод-рантайма. alpineкрошечный за счётmusl libc, ноmuslвместоglibcломает часть нативных пакетов.distrolessбезопаснее всех, потому что в нём нет ниshell, ни пакетного менеджера — но и отлаживать его труднее..dockerignoreуменьшает контекст сборки, ускоряетCOPYи не пускает мусор и секреты в образ.- Объединяйте установку и очистку в один
RUNчерез&&:apt-get clean && rm -rf /var/lib/apt/lists/*,apk add --no-cache,pip install --no-cache-dir. - Очистка работает, только если она в том же слое, что и установка: удаление в новом слое размер образа не уменьшает.