Что такое многоэтапная сборка (Multi-stage builds) в Docker?

Ответ

Многоэтапная сборка (Multi-stage builds) — это функция Docker, позволяющая использовать несколько инструкций FROM в одном Dockerfile. Каждая инструкция FROM начинает новый этап сборки, и можно копировать артефакты (например, скомпилированные бинарники, зависимости) из одного этапа в другой. Основная цель — создать минимальный и безопасный итоговый образ, исключив из него инструменты сборки, промежуточные файлы и ненужные зависимости.

Типичная проблема, которую решает Multi-stage: Без нее в финальном образе остаются компиляторы, заголовочные файлы, пакеты для разработки (например, gcc, python3-dev), которые нужны только для сборки приложения, но не для его запуска. Это увеличивает размер образа и поверхность для атак.

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

# ЭТАП 1: Сборка (Builder Stage)
# Используем полный образ с компилятором
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: Финальный образ (Runtime Stage)
# Используем минимальный образ без компилятора
FROM alpine:latest  
RUN apk --no-cache add ca-certificates  # Добавляем только необходимые для работы пакеты
WORKDIR /root/
COPY --from=builder /myapp .            # Копируем ТОЛЬКО бинарник из этапа сборки
CMD ["./myapp"]

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

# Этап сборки зависимостей
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt  # Устанавливаем зависимости в домашнюю директорию пользователя

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

Ключевые преимущества:

  • Значительное уменьшение размера образа: Финальный образ содержит только рантайм-зависимости и артефакты.
  • Повышение безопасности: Уменьшается количество установленных пакетов и, следовательно, потенциальных уязвимостей.
  • Улучшение структуры и читаемости: Весь процесс сборки и финальной упаковки описан в одном файле, что упрощает CI/CD.

Ответ 18+ 🔞

Давай разберём эту многоэтажную сборку, а то звучит как будто про небоскрёб речь, а на деле — просто хитрая жопа в Docker.

Вот смотри, раньше было как: пишешь Dockerfile, компилируешь в нём своё приложение, а в итоге получаешь образ, который весит овердохуища. Потому что внутри остаётся весь этот сброд: компиляторы, заголовочные файлы, тонны пакетов для разработки. А для запуска-то нужен только готовый бинарник! Это как если бы ты, чтобы съесть суп, тащил на кухню целую свинью, топор и дровосека. Ёперный театр!

Так вот, умные дядьки придумали Multi-stage builds. Суть проста до безобразия: ты разбиваешь сборку на этапы. На первом этапе — в грязном цеху — ты компилируешь, собираешь, паяешь своё приложение. А потом из этого бардака ты забираешь только готовый продукт (бинарник, зависимости) и переносишь его в чистую, стерильную комнату — второй этап. А весь строительный мусор остаётся в прошлом и в финальный образ не попадает. Гениально, ёпта!

Смотри на примере Go. Раньше бы пришлось тащить golang:1.21-alpine в продакшен. А теперь:

# ЭТАП 1: Грязный цех (Builder Stage)
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: Чистая комната (Runtime Stage)
FROM alpine:latest  # Смотри какая крошка, а не образ!
RUN apk --no-cache add ca-certificates  # Только самое необходимое
WORKDIR /root/
COPY --from=builder /myapp .  # Вот магия! Крадём бинарник из первого этапа.
CMD ["./myapp"]

Видишь фокус? COPY --from=builder. Мы как воры в законе — зашли в первый контейнер, вынесли только ценности (/myapp), а весь инструмент (go, исходники) оставили там. Финальный образ — голый alpine с одним бинарником. Разница в размерах — ни хуя себе!

С Python та же история, только там не бинарник, а пакеты из pip:

# Этап 1: Установщик
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Этап 2: Бегун
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local  # Перетащили только установленные либы
COPY src/ .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "main.py"]

Итоговые плюсы, от которых сам от себя охуеешь:

  1. Размер образа. Из жирного хуя в пальто он превращается в субтильного мальчика. Скачивать и разворачивать быстрее.
  2. Безопасность. Меньше пакетов — меньше дыр. Нет в продакшене gcc — нечего ему там делать, пидарас шерстяной.
  3. Читаемость. Всё в одном файле. Видно, где собираем, где запускаем. Не нужно городить три отдельных скрипта.

Короче, если не используешь multi-stage — ты распиздяй. Это теперь must have для любого серьёзного Dockerfile. Всё, что нужно для сборки, остаётся за бортом, а в плавание выходит только готовый корабль. Ебушки-воробушки, как же просто и эффективно!