Как использовать multistage builds для оптимизации Docker-образов?

«Как использовать multistage builds для оптимизации Docker-образов?» — вопрос из категории Docker, который задают на 24% собеседований Devops Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Multistage builds в Docker — это подход, при котором в одном Dockerfile используется несколько инструкций FROM. Цель — отделить этап сборки (где нужны компиляторы, dev-зависимости) от этапа рантайма, скопировав в финальный образ только необходимые артефакты. Это радикально уменьшает размер и повышает безопасность образа.

Базовый пример для Go-приложения:

# Этап 1: Сборка (Builder)
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./ 
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /myapp ./cmd/app

# Этап 2: Финальный, минимальный образ
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Копируем только скомпилированный бинарник из этапа builder
COPY --from=builder /myapp .
CMD ["./myapp"]

В этом примере финальный образ основан на alpine (~5MB) и содержит только бинарник, а не весь Go-тулчейн (~400MB).

Пример для Python-приложения:

# Этап сборки
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-warn-script-location -r requirements.txt

# Финальный этап
FROM python:3.11-slim
WORKDIR /app
# Копируем установленные пакеты из домашней директории пользователя builder
COPY --from=builder /root/.local /root/.local
COPY . .
# Убедимся, что скрипты из pip доступны
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

Ключевые преимущества, которые я наблюдал на практике:

  • Размер: Образы уменьшаются с гигабайтов до десятков мегабайт. Это ускоряет загрузку в registry и развёртывание в кластерах (Kubernetes).
  • Безопасность: В продакшен-образе отсутствуют компиляторы, отладочные инструменты и исходный код, что сокращает поверхность для атаки.
  • Чёткость: Dockerfile явно разделяет зависимости для сборки и для выполнения.
  • Кэширование: Каждый этап кэшируется независимо, что ускоряет повторные сборки.