23. Multi stage сборки Docker
Концепция: Разделяй и властвуй
В традиционном ремесленном производстве мастер никогда не отправляет заказчику кузнечный горн, наковальню и груду шлака вместе с готовым мечом. Покупателю нужно лишь само отточенное лезвие. Долгое время в мире Docker эта очевидная истина нарушалась: разработчикам приходилось либо паковать в финальный образ весь сборочный цех (тяжеловесный тулчейн компилятора, исходные коды и менеджеры пакетов), либо прибегать к неуклюжему паттерну "Builder Container", извлекая бинарник на хост-машину с помощью внешних shell-скриптов и временных томов.
С появлением механизма многоэтапных сборок (Multi-stage builds) в Docker и тулчейне Go разработчики получили элегантный инструмент разделения обязанностей прямо внутри единого декларативного Dockerfile.
Идея предельно проста:
- Этап 1 (Builder): Полнофункциональное сборочное окружение с компилятором Go, заголовочными файлами, утилитой Git и кэшем модулей. Здесь происходит выкачивание зависимостей и компиляция.
- Этап 2 (Runtime): Сверхлегковесный или абсолютно чистый образ (Alpine, Scratch или Distroless), куда переносится исключительно скомпилированный бинарный файл и необходимые системные метаданные.
Все промежуточные гигабайты сборочного цеха мгновенно выбрасываются сборщиком, оставляя для развертывания компактный и безопасный артефакт.
Архитектура Multi-Stage сборки
Многоэтапный Dockerfile строится с использованием нескольких независимых директив FROM. Каждому этапу можно присвоить символическое имя через конструкцию AS <name>:
# --- Stage 1: Builder ---
FROM golang:1.22-alpine AS builder
# Устанавливаем git (нужен для go get в частных случаях) и ca-certificates
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# Кэшируем зависимости
COPY go.mod go.sum ./
RUN go mod download
# Копируем исходники и собираем
COPY . .
# Важная часть: отключаем CGO для статической линковки
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /myapp ./cmd/app
# --- Stage 2: Runtime ---
FROM alpine:latest
# Устанавливаем сертификаты и timezone данные (для работы с HTTPS и временем)
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /root/
# Копируем бинарник из первого этапа
COPY --from=builder /myapp .
# Копируем статику или конфиги при необходимости
# COPY --from=builder /app/static ./static
CMD ["./myapp"]
Визуализация процесса
flowchart TD
subgraph Builder ["Stage 1: Builder golang:alpine ~400MB"]
A["go mod download"] --> B["go build"]
B --> C["Артефакт: /myapp"]
end
subgraph Runtime ["Stage 2: Runtime alpine ~5MB"]
D["FROM alpine"]
E["COPY --from=builder /myapp"]
end
C --> E
F["Финальный образ"] --> G["Весит около 15-20MB"]
G --> H["Содержит только бинарник и ОС"]
style Builder fill:#1e293b,stroke:#3b82f6,color:#fff
style Runtime fill:#1e293b,stroke:#10b981,color:#fff
style F fill:#374151,stroke:#9ca3af,color:#fff
Сравнение базовых образов для Runtime
Выбор основы для релизного слоя — это классический инженерный компромисс между удобством сопровождения, функциональностью и информационной безопасностью:
| Образ | Размер | Shell | glibc | Безопасность | Комментарий |
|---|---|---|---|---|---|
| golang:1.22 | ~1.2 GB | Да | Да | Низкая | Никогда не используйте в runtime. |
| alpine:latest | ~5 MB | Да sh |
musl | Средняя | Требует CGO_ENABLED=0. Ошибки DNS/Timezone решаются установкой пакетов. |
| scratch | 0 MB | Нет | - | Максимальная | Самый безопасный. Нет shell, нельзя сделать exec -it. |
| distroless | ~2 MB | Нет | glibc | Очень высокая | От Google. Есть варианты с debug тегом (busybox). |
Distroless: Золотой стандарт продакшена
Проект Distroless, разработанный инженерами Google, предлагает золотую середину между абсолютной пустотой scratch и самодостаточностью стандартных дистрибутивов.
Образы gcr.io/distroless/static или gcr.io/distroless/base созданы специально для безопасного запуска сервисов в облаке.
Они содержат минимально необходимый фундамент:
- Доверенные корневые SSL/TLS CA-сертификаты (
/etc/ssl/certs/ca-certificates.crt). - Базовые системные файлы
/etc/passwdи/etc/groupдля запуска от непривилегированного пользователя. - Информацию о часовых поясах (
tzdata).
И принципиально не содержат:
- Никаких командных оболочек (
sh,bash). - Пакетных менеджеров (
apk,apt,yum). - Системных утилит ядра (
curl,wget,nc,rm,ls).
# Runtime stage with Distroless
FROM gcr.io/distroless/static-debian12:latest
COPY --from=builder /myapp /
CMD ["/myapp"]
Если злоумышленник сумеет внедрить эксплойт через прикладной протокол, он окажется в глухом капкане: в файловой системе контейнера нет ни единого бинарника или шелла для закрепления в системе или загрузки вредоносного кода.
Итог
- Multi-stage builds — бескомпромиссный стандарт сборки контейнеров для компилируемых языков, избавляющий финальный образ от сборочного тулчейна.
- Именуйте промежуточные этапы осмысленно (
AS builder), чтобы директивыCOPY --from=builderсохраняли выразительность. - Соблюдайте порядок кэширования: сначала копируйте
go.modи выполняйтеgo mod download, а затем переносите исходный код. - Выбирайте базовый образ рантайма осознанно:
distrolessилиscratchдля боевых кластеров,alpine— если локально требуется базовыйshдля регламентных работ.
Мы научились собирать лаконичные и защищенные контейнеры. Теперь стоит глубже исследовать тонкости работы с минимальными окружениями: почему отсутствие /etc/passwd может удивить рантайм Go, как устроены корневые сертификаты и когда стоит предпочесть distroless образу scratch.
В следующей статье мы погрузимся в детали экстремального сжатия: 24. Минимизация образов. scratch, distroless.