Лучшие практики Dockerfile: быстрые и чистые образы
Практика3 минРедакция Innovialabs
Коротко
Хороший Dockerfile копирует файлы зависимостей раньше кода, использует компактный базовый образ с зафиксированной версией, исключает лишнее через .dockerignore и запускает приложение не от root.
Коротко: что делает Dockerfile хорошим
Быстрый и чистый образ получается, если соблюдать пять правил:
- Порядок слоёв: то, что меняется редко, — наверх, то, что часто, — вниз.
- Компактный базовый образ с конкретной версией.
- .dockerignore, чтобы не тащить в образ мусор и секреты.
- Непривилегированный пользователь вместо root.
- Зафиксированные версии базового образа и зависимостей.
Порядок слоёв и кэш
Docker собирает образ по слоям и кэширует каждый. Если слой изменился, все слои после него пересобираются. Поэтому сначала копируйте только файлы зависимостей и устанавливайте их, а уже потом — исходный код.
Плохо: COPY . . в начале. Любая правка в коде заставит заново ставить все пакеты.
Хорошо: package.json и lock-файл отдельно, установка, затем код. Пока зависимости не меняются, установка берётся из кэша.
Ещё советы:
- Объединяйте связанные команды в один
RUNи чистите кэш пакетного менеджера в том же слое. - Не устанавливайте инструменты, которые нужны только для сборки, в финальный образ — используйте multi-stage build.
Выбор базового образа
| Вариант | Плюсы | Минусы |
|---|---|---|
Полный (node:22, python:3.12) | Всё есть «из коробки» | Большой размер, больше уязвимостей |
Slim (-slim) | Меньше, на базе Debian | Иногда не хватает системных пакетов |
Alpine (-alpine) | Очень маленький | musl вместо glibc, возможны проблемы с нативными модулями |
| Distroless | Минимальная поверхность атаки | Нет shell, сложнее отлаживать |
Для большинства приложений разумный старт — slim или alpine с официальным тегом конкретной версии.
.dockerignore
Без него в контекст сборки попадает всё: node_modules, .git, локальные .env. Сборка медленнее, образ больше, секреты могут утечь.
node_modules
.git
.env
*.log
dist
__pycache__
.venv
Пример для Node.js
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=node:node . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Что здесь важно:
- Первая стадия ставит зависимости по lock-файлу через
npm ci— воспроизводимо. - Финальная стадия получает только готовые
node_modulesи код. USER node— встроенный непривилегированный пользователь официального образа.CMDв exec-форме (массив) — процесс корректно получает сигналы остановки.
Если проекту нужна сборка (TypeScript, фронтенд), добавьте отдельную стадию build и копируйте в финальный образ только результат.
Пример для Python
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN useradd --create-home appuser
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]
Что здесь важно:
requirements.txtкопируется до кода — установка кэшируется.--no-cache-dirне оставляет кэш pip в образе.PYTHONUNBUFFERED=1— логи сразу попадают в stdout контейнера.- Приложение работает от отдельного пользователя
appuser.
Частые ошибки
- Тег
latestдля базового образа — сборка меняется без вашего ведома. - Секреты в
ENVилиCOPY— они остаются в слоях образа даже после удаления. apt-get updateв отдельномRUNотapt-get install— устаревший кэш индекса.- Один огромный образ с компиляторами и dev-зависимостями в продакшене.