Короткий ответ
Стадия сборки использует полнофункциональный образ с компилятором и зависимостями, а финальная стадия строится на минимальном base image (alpine, distroless, scratch), в который копируется только скомпилированный бинарник через COPY --from=, без инструментов сборки и промежуточных артефактов.
Как это работает подробнее
промежуточная стадия (builder) может весить сотни мегабайт за счёт компилятора, системных заголовков и зависимостей сборки, но в финальный образ эти слои не попадают — копируется только явно указанный путь. Для статически линкуемых языков (Go, Rust) финальной стадией может быть даже scratch — полностью пустой базовый образ, содержащий только бинарник.
Пример кода
Dockerfile
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app .
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app /app
ENTRYPOINT ["/app"]Что использовать на практике
- Практика. это значительно сокращает размер итогового образа (с сотен мегабайт до единиц–десятков), ускоряет
docker pullв production и уменьшает поверхность атаки — в финальном образе физически отсутствуют компиляторы, пакетные менеджеры и build-инструменты, которые могли бы быть использованы при компрометации.
Подводные камни
- Риск. забывают скопировать вспомогательные файлы, нужные бинарнику в runtime (например, сертификаты CA для TLS-соединений при использовании
scratch, которые обычно берутся из builder-стадии черезCOPY --from=builder /etc/ssl/certs/ca-certificates.crt ...); используют слишком "толстый" финальный базовый образ (например, полныйgolangвместоalpine/scratch) и теряют выгоду multi-stage build.