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, а получает только явно скопированный артефакт.