ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Multi-stage build Docker: собираем Go-приложение и отделяем runtime

Автор: · Обновлено

Multi-stage build (multistage, multi stage builds) делит сборку на стадии. В одной находятся компилятор и исходники; в конечную копируется только результат и необходимые…

Multi-stage build (multistage, multi stage builds) делит сборку на стадии. В одной находятся компилятор и исходники; в конечную копируется только результат и необходимые runtime-файлы. Выигрыш — контролируемый состав образа, а не магическое ускорение любой команды. Покажем это на полностью самостоятельной программе Go без внешних модулей.

Исходник

Создайте main.go в новой папке:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "multi-stage-ready")
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Это учебный HTTP-сервис. Для production к нему потребуются явно выбранные таймауты, корректное завершение и другие свойства вашего приложения; здесь проверяется именно разделение build/runtime.

Две стадии Dockerfile

FROM golang:1.26-alpine AS build
WORKDIR /src
COPY main.go ./main.go
RUN CGO_ENABLED=0 go build -trimpath -o /out/server ./main.go

FROM scratch AS runtime
COPY --from=build /out/server /server
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["/server"]

AS build даёт стадии имя. COPY --from=build читает результат именно из неё. Финальная scratch-основа пуста; здесь нет shell, Go toolchain и менеджера пакетов. Наш сервер может работать так, поскольку сборка отключает cgo, а программа не читает системные сертификаты, временные зоны и пользовательские файлы. Это предпосылки примера, а не правило для всех Go-приложений. Механизм multi-stage.

Проверка результата

docker build -t dk-multistage:1 .
docker run -d --name dk-multi-web -p 127.0.0.1:18089:8080 dk-multistage:1
curl http://127.0.0.1:18089/
docker image inspect dk-multistage:1 --format '{{.Size}}'
docker stop dk-multi-web
docker rm dk-multi-web

Ответ — multi-stage-ready. При слишком раннем запросе дождитесь запуска процесса и повторите curl. Размер выводится в байтах; он зависит от toolchain и архитектуры. Чтобы сравнить состав, отдельно соберите стадию build:

docker build --target build -t dk-multistage:build .
docker run --rm dk-multistage:build go version
docker image inspect dk-multistage:build --format '{{.Size}}'

В build-образе доступен Go, в runtime — только скопированный исполняемый файл и метаданные. Не пытайтесь исследовать scratch через docker exec ... sh: отсутствие shell ожидаемо. Используйте logs, inspect и отдельную отладочную сборку.

Alpine, Debian slim и distroless: выбор основы

Размер образа имеет смысл сравнивать вместе с совместимостью приложения. Alpine обычно использует musl libc, Debian slim — glibc. Нативный Python wheel или бинарник, собранный под glibc, нельзя считать совместимым с musl только потому, что обе основы называются Linux. Если для Alpine нет готового подходящего wheel, установка может потребовать компилятора и дополнительных библиотек; выигрыш размера не гарантирует более быструю сборку. Варианты официального Python-образа.

Основа runtimeЧто получаемЧто проверить перед выбором
AlpineНебольшая пользовательская среда, shell, apkСовместимость musl и нативных модулей
Debian slimСокращённая Debian-среда с glibc и aptНужные shared libraries, CA и runtime-пакеты
DistrolessПодобранные runtime-файлы без обычного shell и package managerВариант языка/ОС, UID, сертификаты и способ отладки
scratchПустая файловая основаСамодостаточность бинарника и все читаемые им файлы

Distroless — семейство минимальных runtime-образов проекта GoogleContainerTools, а не отдельная ОС и не синоним scratch. Выбирают подходящий образ под язык и нужные библиотеки; nonroot-вариант явно задаёт непривилегированного пользователя. Отсутствие shell уменьшает набор инструментов внутри, но не доказывает отсутствие уязвимостей. Документация distroless.

Не рассчитывайте на docker exec ... sh в обычном distroless runtime. Планируйте логи, inspect, внешние средства диагностики или специальный debug-вариант для своей среды. Проверьте HTTPS-запрос с доверенными CA, запись только в разрешённый каталог и наличие runtime-библиотек. Сравнивайте размер и запуск двух совместимых конечных образов, сохранив одинаковый артефакт приложения; универсального победителя между Alpine и slim нет.

Где схема ломается

Если бинарник связан с динамическими библиотеками, копирования одного файла недостаточно. Ошибка no such file or directory при существующем бинарнике может означать отсутствующий динамический загрузчик. Отдельная причина exec format error — несовпадение архитектуры. Перенос сборки с arm64-ноутбука на amd64-сервер требует выбора платформы и проверки результата. Платформы сборки.

Для Python multi-stage обычно собирает wheels в builder и устанавливает их в совместимый runtime. Нельзя копировать виртуальное окружение между произвольными дистрибутивами: ABI, версия Python, libc и нативные библиотеки должны подходить. Аналогично Go с cgo может требовать runtime-библиотек. Multi-stage не отменяет зависимости — он делает их отбор вашей ответственностью.

Если приложение обращается по HTTPS, ему могут понадобиться корневые сертификаты; если читает временные зоны — база tzdata. Выбирайте минимальную достаточную основу, а не scratch ради самого маленького числа.

Упражнение

Добавьте второй маршрут /health, соберите тег :2 и проверьте оба ответа. Сравните размер build и runtime, объясните происхождение разницы. Затем ответьте, почему удаление компилятора командой RUN в позднем слое одного образа не даёт того же результата: предыдущие слои уже содержат добавленные файлы. В multi-stage конечный образ вообще не наследует стадию build, а получает только явно скопированный артефакт.

Источники