← Назад к вопросам

Сталкивался с Multi-stage builds, когда в образе два раза есть FROM

1.7 Middle🔥 201 комментариев
#CI/CD и автоматизация#Docker и контейнеризация

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Конечно, сталкивался. Multi-stage builds — это один из ключевых инструментов в арсенале DevOps-инженера для создания оптимальных Docker-образов. Прямой ответ на ваш вопрос — да, я не только сталкивался, но и активно применяю эту технику в продакшене для решения критически важной задачи: отделения среды сборки (build environment) от среды выполнения (runtime environment).

Суть и проблема, которую решает Multi-stage Build

Раньше, используя обычный Dockerfile, мы часто получали раздутые образы. Ведь в них оставались все зависимости для компиляции, инструменты сборки (вроде GCC, Maven, npm), исходный код и промежуточные артефакты. Среда выполнения же (например, для Java-приложения) требует лишь JRE и собранный JAR-файл, а для Go-приложения — только один бинарный файл.

Multi-stage build решает это, позволяя использовать несколько инструкций FROM в одном Dockerfile. Каждая инструкция FROM начинает новый этап (stage) сборки. Вы можете копировать артефакты из одного этапа в другой, оставляя всё ненужное позади.

Конкретный пример: Go-приложение

Рассмотрим классический пример на Go, где преимущества видны максимально четко.

# Этап 1: Сборка (Builder Stage)
# Используем полный образ с Go для компиляции
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 -a -installsuffix cgo -o /myapp .

# Этап 2: Запуск (Final/Runtime Stage)
# Используем минимальный образ без лишних инструментов
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# Копируем ТОЛЬКО скомпилированный бинарник из этапа 'builder'
COPY --from=builder /myapp .
# Определяем команду для запуска
CMD ["./myapp"]

Что здесь происходит:

  1. Первый этап (builder):
    *   Берется образ `golang:1.21-alpine` (около 350 МБ).
    *   В нем скачиваются зависимости, компилируется приложение. На выходе — бинарный файл `myapp`.
  1. Второй этап:
    *   Начинается с чистого, минимального образа `alpine:latest` (около 5-7 МБ).
    *   Ключевая инструкция `COPY --from=builder` извлекает **только итоговый бинарник** из первого этапа.
    *   Компилятор Go, исходный код, кэш модулей — всё это остается в первом этапе и **не попадает в финальный образ**.

Итог: Финальный образ весит не 350+ МБ, а всего ~10-12 МБ (Alpine + бинарник). Это колоссальная оптимизация.

Продвинутые сценарии использования

  • Именование этапов (AS <name>): Позволяет явно ссылаться на нужный этап при копировании (--from=builder).
  • Несколько промежуточных этапов:
    FROM node:18 AS deps
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    
    FROM node:18 AS builder
    COPY --from=deps /app/node_modules ./node_modules
    COPY . .
    RUN npm run build
    
    FROM nginx:alpine AS runtime
    COPY --from=builder /app/dist /usr/share/nginx/html
    
    Здесь четко разделены этапы: установка `deps`, `builder` для сборки и легковесный `runtime` на nginx.
  • Использование внешних образов как этапа: Можно копировать файлы не из предыдущего этава этого же Dockerfile, а из любого образа.
    COPY --from=redis:alpine /usr/local/bin/redis-cli /usr/local/bin/redis-cli
    
  • Уязвимости и безопасность: Минимальный финальный образ имеет значительно меньшую атакующую поверхность (attack surface). В нем просто нет утилит вроде bash, curl или компиляторов, которые могли бы быть использованы злоумышленником при компрометации контейнера.

Практические советы и лучшие практики

  • Целевой образ. Для второго этапа почти всегда выбирайте минимальные базовые образы: alpine, distroless (от Google), scratch (пустой образ для статичных бинарников).
  • Кэширование слоев. Правильно структурируйте команды COPY и RUN, чтобы зависимости кэшировались до копирования часто меняющегося исходного кода. В примере с Go go.mod копируется отдельно.
  • docker build --target. Эта мощная опция позволяет остановиться на любом промежуточном этапе. Например, для запуска тестов в CI/CD:
    docker build --target builder -t myapp:builder .
    
    Это создаст образ только до этапа `builder`, где есть все инструменты для тестирования.
  • Артефакты. Помимо бинарников, между этапами можно переносить отчеты о тестировании, лицензии, конфигурационные файлы.

В заключение: Multi-stage builds — это не просто синтаксический сахар, а фундаментальный паттерн, который напрямую влияет на безопасность, скорость развертывания (меньший образ = быстрая загрузка по сети) и стоимость хранения образов в реестрах. Его использование — признак зрелого подхода к контейнеризации.

Сталкивался с Multi-stage builds, когда в образе два раза есть FROM | PrepBro