Какой у вас опыт с multi-stage сборками в Dockerfile?

«Какой у вас опыт с multi-stage сборками в Dockerfile?» — вопрос из категории Docker, который задают на 28% собеседований Devops Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Я активно использую multi-stage сборки для создания минимальных и безопасных production-образов. Это стандартный подход в моих проектах.

Основная цель: отделить среду сборки (со всеми компиляторами, зависимостями для разработки) от финального рантайм-образа. Это резко уменьшает итоговый размер и площадь для потенциальных уязвимостей.

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

# STAGE 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 -ldflags="-s -w" -o main ./cmd/app

# STAGE 2: Final (Runtime)
FROM scratch
WORKDIR /
COPY --from=builder /app/main .
COPY --from=builder /app/configs ./configs
# Копируем SSL сертификаты, если нужно
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
CMD ["./main"]

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

  1. Размер образа: Образ на основе scratch или alpine весит несколько мегабайт вместо гигабайта (как образ golang:1.21). Это ускоряет загрузку и развёртывание.
  2. Безопасность: В финальном образе нет компилятора, пакетного менеджера или shell (если используется scratch), что минимизирует векторы для атаки.
  3. Чистота: Образ содержит только скомпилированный бинарник и необходимые для его работы файлы (конфиги, сертификаты).

Нюансы и опыт:

  • Можно использовать более двух стадий. Например, одна стадия для установки зависимостей, вторая для сборки, третья — финальная.
  • Важно копировать из промежуточных стадий только нужные артефакты (бинарники, статические файлы).
  • Для приложений на интерпретируемых языках (Python, Node.js) подход аналогичен: на одной стадии устанавливаются все dev-зависимости и происходит сборка (например, минификация JS), а на финальную копируется только код и production-зависимости.