Docker: multi-stage builds

В Docker 17.05 добавили приятную функциональность под названием multi-stage builds. Заключается она в том, что теперь можно объединить несколько Dockerfile в один, так сказать, “многоступенчатый” Dockerfile с возможностью копирования файлов между стадиями.

Полезно это тогда, когда в процессе сборки образа нужно больше софта, чем для последующего запуска. Типичный пример — компиляция в контейнерах. Нужна куча всяких SDK, библиотек, тулов, а кроме того генерируется масса промежуточных артефактов.

Давайте рассмотрим пару примеров.

Приложение на ASP.NET Core из исходников

# Стадия 1: сборка

# Используем базовый образ aspnetcore-build с предустановленным .NET Core SDK
# as builder - идентификатор стадии
FROM microsoft/aspnetcore-build:1.0 as builder

WORKDIR app1

# Создаем с нуля простейшее MVC-приложение
# В "реальном мире" тут был бы git clone
RUN dotnet new mvc --framework netcoreapp1.0
RUN echo "<h1>Hello. I'm in Docker</h1>" > Views/Home/Index.cshtml

# Рутинные команды .NET Core SDK
RUN dotnet restore
RUN dotnet publish -c Release

# Стадия 2: финальный образ

# База содержит только .NET Core runtime
FROM microsoft/aspnetcore:1.0

WORKDIR app1

# Копируем результат публикации из стадии builder к себе
COPY --from=builder /app1/bin/Release/netcoreapp1.0/publish .

CMD dotnet app1.dll

Собираем:

docker build -t sample1 .

Получились два образа — итоговый sample1 и промежуточный без тега (его легко удалить командой docker image prune):

docker images

REPOSITORY    TAG       IMAGE ID        CREATED           SIZE
sample1       latest    7dfc076834d9    22 seconds ago    306MB
<none>        <none>    0ba10b44fe5d    23 seconds ago    1.09GB

Выгода налицо: 300 Мб против 1 Гб.

Проверяем в действии:

CID=$(docker run -d -p 5000:80 sample1)

Открываем http://localhost:5000 в браузере или через curl / wget.

Убедившись, что всё работает, прибираем за собой:

docker rm -f $CID
docker rmi sample1

Go и Scratch

Более радикальную операцию можно провести с языком Go, который позволяет делать статически связанные бинарники:

FROM golang as builder
RUN go get github.com/golang/example/hello
RUN CGO_ENABLED=0 go build -a github.com/golang/example/hello

FROM scratch
COPY --from=builder /go/bin/hello /
ENTRYPOINT [ "/hello" ]

В результате получится максимально “худой” образ — в нём будет только один файл, не требующий даже libc.

docker build -t sample2 .
docker run --rm sample2

Hello, Go examples!

Я готов засвидетельствовать, что так можно запускать не только hello world, но и настоящие приложения.