Сталкивался с Multi-stage builds, когда в образе два раза есть FROM
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Конечно, сталкивался. 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"]
Что здесь происходит:
- Первый этап (
builder):
* Берется образ `golang:1.21-alpine` (около 350 МБ).
* В нем скачиваются зависимости, компилируется приложение. На выходе — бинарный файл `myapp`.
- Второй этап:
* Начинается с чистого, минимального образа `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, чтобы зависимости кэшировались до копирования часто меняющегося исходного кода. В примере с Gogo.modкопируется отдельно. docker build --target. Эта мощная опция позволяет остановиться на любом промежуточном этапе. Например, для запуска тестов в CI/CD:docker build --target builder -t myapp:builder .
Это создаст образ только до этапа `builder`, где есть все инструменты для тестирования.
- Артефакты. Помимо бинарников, между этапами можно переносить отчеты о тестировании, лицензии, конфигурационные файлы.
В заключение: Multi-stage builds — это не просто синтаксический сахар, а фундаментальный паттерн, который напрямую влияет на безопасность, скорость развертывания (меньший образ = быстрая загрузка по сети) и стоимость хранения образов в реестрах. Его использование — признак зрелого подхода к контейнеризации.