DevOps

Docker: образ, который не весит гигабайт

28.08.2026 · TitarX · 👁 3

Образ на 1.2 ГБ для приложения, которое умещается в несколько мегабайт исходников, — обычное дело. Причина почти всегда одна: в финальный образ попало то, что нужно было только во время сборки.

Как устроены слои

Каждая инструкция RUN, COPY и ADD создаёт новый слой. Слои неизменяемы: если вы установили пакеты в одном слое и удалили их в следующем, данные останутся в образе — удаление лишь пометит их отсутствующими на верхнем уровне.

Отсюда классическая ошибка:

# плохо: кэш apt остаётся в предыдущем слое
RUN apt-get update && apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*

Правильно — очищать в той же инструкции, где создавали:

RUN apt-get update \
    && apt-get install -y --no-install-recommends build-essential \
    && rm -rf /var/lib/apt/lists/*

Многослойная сборка

Основной приём — разделить сборку и запуск. Компиляторы, заголовочные файлы и тулчейн нужны только на первом этапе, а в финальный образ переносится лишь результат:

# --- этап сборки ---------------------------------------------------
FROM python:3.12-slim AS builder

WORKDIR /app
RUN apt-get update \
    && apt-get install -y --no-install-recommends build-essential \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# --- финальный образ -----------------------------------------------
FROM python:3.12-slim

# Непривилегированный пользователь: процесс в контейнере не должен быть root
RUN useradd --create-home --uid 1000 app
WORKDIR /app

COPY --from=builder /install /usr/local
COPY --chown=app:app . .

USER app
EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]

build-essential остался на этапе сборки и в финальный образ не попал — обычно это уже сотни мегабайт разницы.

Порядок инструкций и кэш

Docker кэширует слои и переиспользует их, пока не изменились входные данные. Как только один слой признан изменившимся, все последующие пересобираются заново.

Практический вывод: сначала копируйте то, что меняется редко. Файл зависимостей — отдельно от исходников:

COPY requirements.txt .               # меняется редко
RUN pip install -r requirements.txt   # кэш переживает правки кода
COPY . .                              # меняется при каждом коммите

Обратный порядок означает переустановку всех зависимостей после каждой правки в коде.

.dockerignore

Файл, о котором забывают чаще всего. Без него в контекст сборки уезжает вся папка целиком — вместе с .git, виртуальным окружением и базой данных:

.git
.venv
__pycache__/
*.pyc
db.sqlite3
.env
node_modules
staticfiles/

Помимо размера, здесь есть вопрос безопасности: .env, попавший в образ, — это утёкшие секреты.

Проверка живости

Оркестратор должен понимать разницу между «процесс запущен» и «приложение отвечает»:

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
    CMD curl -fsS http://localhost:8000/health/ || exit 1

Что проверить в своём Dockerfile

  • есть ли .dockerignore;
  • разделены ли этапы сборки и запуска;
  • копируются ли зависимости раньше исходников;
  • выполняется ли очистка в той же инструкции, что и установка;
  • запускается ли процесс не от root;
  • зафиксированы ли версии базовых образов (python:3.12-slim, а не python:latest).

Что в итоге

Размер образа — это не эстетика: он влияет на время деплоя, стоимость хранения и площадь атаки. Многослойная сборка, аккуратный порядок инструкций и .dockerignore дают основной выигрыш и занимают полчаса работы один раз.