Docker: образ, который не весит гигабайт
Образ на 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 дают основной выигрыш и
занимают полчаса работы один раз.