Что такое Multi-stage builds в Docker?

Ответ

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

Проблема, которую это решает: Традиционный Dockerfile для компилируемого языка (Go, C++) включает в финальный образ компилятор, исходный код и все зависимости для сборки, что сильно увеличивает размер и поверхность для атак.

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

# --- СТАДИЯ 1: Сборка (builder) ---
# Используем полный образ с 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 main .

# --- СТАДИЯ 2: Запуск (final) ---
# Используем пустой минимальный образ
FROM scratch
WORKDIR /root/
# Копируем ТОЛЬКО скомпилированный бинарник из стадии 'builder'
COPY --from=builder /app/main .
# Копируем SSL-сертификаты (необходимы для многих сетевых вызовов)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Запускаем приложение
CMD ["./main"]

Преимущества:

  • Размер: Итоговый образ уменьшается с ~350 МБ (полный Go-образ) до ~10 МБ (один бинарник).
  • Безопасность: В финальном образе нет компилятора, shell или пакетного менеджера, что сокращает векторы атак.
  • Чистота: В образе только то, что нужно для запуска приложения.

Ответ 18+ 🔞

Давай разберём эту штуку, которая в Docker называется multi-stage builds. Представь, что ты строишь дом. Сначала ты таскаешь на участок кучу инструментов: бетономешалку, леса, горы досок, весь этот срач. А когда дом построен, ты же не оставляешь на лужайке эту всю хуйню? Ты её убираешь, и остаётся только чистый, красивый дом. Так вот, multi-stage builds — это как раз про это.

В чём, блядь, была проблема? Раньше, если ты писал на Go или C++, твой Dockerfile был пиздец какой неэффективный. Ты брал здоровенный образ с компилятором, всеми библиотеками, собирал в нём своё приложение, а потом этот же образ, набитый строительным мусором, ты и отправлял в продакшн. Получался монстр на 350 мегабайт, в котором валялся компилятор, исходники и прочая хрень, которой для запуска-то и не нужно. И поверхность для атаки — овердохуища, любой желающий мог там поковыряться.

А как теперь, по-умному? Теперь можно в одном Dockerfile делать несколько стадий. Каждая начинается со своего FROM. Первая стадия — это наша стройплощадка со всем инструментарием. Вторая — уже чистый, голый образ, куда мы закинем только готовый продукт. Итог? Образ в 10 МБ вместо 350. Безопасность? Да похуй уже кому там ковыряться, там нихуя нет, кроме самого бинарника.

Смотри на примере, как это выглядит:

# --- СТАДИЯ 1: Стройплощадка (builder) ---
# Берём здоровенный образ с 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 main .

# --- СТАДИЯ 2: Результат (final) ---
# А это уже наш чистый, голый дом. Берём образ `scratch` — это вообще пустота.
FROM scratch
WORKDIR /root/
# И вот магия: копируем ТОЛЬКО готовый бинарник из стадии 'builder'
COPY --from=builder /app/main .
# SSL-сертификаты тоже нужны, без них в интернете нихуя не сделаешь
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# И запускаем
CMD ["./main"]

И что мы, блядь, выиграли?

  • Размер: С 350 МБ до 10. Разница — просто пиздец. Ты экономишь и место, и время на загрузке.
  • Безопасность: В финальном образе нет ни шелла, ни компилятора, ни пакетного менеджера. Хакеру просто не за что зацепиться, доверия к такому образу — ебать ноль, в хорошем смысле.
  • Чистота: Всё как у хорошего хозяина: в образе лежит ровно то, что нужно для работы приложения, и ни грамма лишнего. Никакого строительного мусора. Красота, ёпта.