Многоступенчатая сборка Docker-образов / Multi-Stage Docker Builds

Это особенно актуально для приложений, разработка которых ведется на компилируемых языках программирования. Используя эту возможность, вы сможете существенно сокращать размер вашего итогового образа не, прибегая к хитрым трюкам, которые я описывал в статье «6 советов по уменьшению Docker образа» Суть подхода заключается в том, чтобы не заботиться о количестве получающихся слоев в процессе сборки вашего приложения и копировать результаты сборки из одного образа в другой. В этой статье я покажу, как это реализуется на практике. Выдумывать ничего не буду, а просто покажу вам это на уже готовых примерах из официальной документации.

Процесс до появления многоэтапных сборок

Одной из самых сложных задач по созданию Docker образов является уменьшение размера итогового образа, ведь каждая команда в Dockerfile добавляет отдельный слой к итоговому образу. Поэтому нам всегда нужно помнить, что необходимо очищать любые не нужные нам артефакты в текущем слое, прежде чем перейти к следующему. Чтобы написать действительно эффективный Dockerfile, нам традиционно необходимо было использовать кучу shell-трюков, чтобы с одной стороны сделать как можно меньше слоев, а с другой, чтобы оставить в каждом созданном слое минимальное количество артефактов.

Вообще говоря, это очень распространенная практика использовать несколько Dockerfile-ов в одном проекте: один для разработки (содержит все необходимое для создания вашего приложения), другой оптимизированный для создания итогового образа, который будет использоваться в продуктиве, в котором должно быть только ваше приложение и все то, что необходимо для его запуска. Эта практика в свое время получила название «шаблон строителя». Однако, поддержание в актуальном состоянии двух Dockerfile-ов не есть что-то удобное.

Вот пример двух Dockerfile-ов (Dockerfile.build и Dockerfile), речь о которых идет выше:

Dockerfile:


FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]

Dockerfile.build:


FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN go get -d -v golang.org/x/net/html \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

И конечно же скрипт, автоматизирующий всю работу:

build.sh:


#!/bin/sh
echo Building alexellis2/href-counter:build

docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \  
    -t alexellis2/href-counter:build . -f Dockerfile.build

docker create --name extract alexellis2/href-counter:build  
docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app  
docker rm -f extract

echo Building alexellis2/href-counter:latest

docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app

Когда вы запускаете build.sh скрипт, он создает первый образ, делает из него контейнер, чтобы скопировать артефакты, а затем создать второй образ. Оба образа занимают место на вашей системе, и еще какое-то место у вас занимают артефакты приложения на вашем локальном диске.

Многоэтапные сборки значительно упрощают эту ситуацию!

Использование многоэтапных (multi-stage) сборок

При многоэтапной сборке вы используете несколько операторов FROM в вашем Dockerfile. Каждая инструкция FROM использует произвольный базовый образ и начинает новый этап сборки. Вы можете выборочно копировать артефакты с одного этапа на другой, оставляя только то, что вам необходимо в конечном образе. Чтобы показать, как это работает, давайте адаптируем Dockerfile из предыдущего раздела для этого процесса.

Dockerfile:


FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]

Теперь вам нужен только один Dockerfile. Более того, вам больше не нужен и отдельный скрипт сборки. Просто запустите сборку образа привычной командой.

docker build -t avmaksimov/href-counter:latest .

Конечным результатом вашей является точно такой же крошечный продуктивный образ, однако сложность процесса значительно сократилась. Теперь вам не нужно создавать промежуточные образы, и вам больше не нужно извлекать промежуточные артефакты на вашу локальную систему.

Как это работает? Вторая команда FROM начинает новый этап сборки с базового образа alpine:latest. Инструкция COPY — from = 0 копирует артефакты с предыдущего этапа сборки на текущий этап, благодаря чему необходимые для сборки Go SDK и любые промежуточные артефакты не сохраняются в конечном образе.

Именование этапов сборки

По умолчанию этапы никак не называются, и вы ссылаетесь на них по их целочисленному номеру, начиная с 0 для первой инструкции FROM. Тем не менее, у вас есть возможность давать своим этапам понятные имена, добавив как в команду FROM. Этот пример улучшает предыдущий, используя имена для этапов. Это также означает, что даже если порядок инструкций в вашем Dockerfile со временем изменится, инструкции COPY не сломаются.

Dockerfile:


FROM golang:1.7.3 as builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"] 

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

Многоступенчатая сборка Docker-образов

В прошлом я уже говорил о создании миниатюрных Docker-образов, но теперь, когда у Docker появилась многоступенчатая сборка, пришла пора вернуться к этому вопросу. Раньше нам приходилось создавать бинарный файл на одном шаге и собирать docker-образ на другом. Это было немного неудобно и требовало некоторых ухищрений. Давайте посмотрим, как многоступенчатые сборки улучшили ситуацию.

Замечание. Для этого нужен Docker 17.05 или более поздний.

Начнем с простой программы на Go:

package mainimport "fmt"func main() {
fmt.Println("Hello world!")
}

Соберем ее с помощью образа golang:alpine одноступенчатым способом (single-stage build). Вот Dockerfile:

FROM golang:alpine
WORKDIR /app
ADD . /app
RUN cd /app && go build -o goapp
ENTRYPOINT ./goapp

Теперь соберем образ и запустим контейнер:

docker build -t treeder/hello .
docker run --rm treeder/hello

Все работает, но давайте посмотрим на размер с помощью docker images | grep treeder/hello.

258 МБ — многовато для крошечного бинарного файла. Теперь давайте попробуем многоступенчатуюсборку (multi-stage build) с использованием нового Dockerfile:

# стадия сборки
FROM golang:alpine AS build-env
ADD . /src
RUN cd /src && go build -o goapp

# финальная стадия
FROM alpine
WORKDIR /app
COPY --from=build-env /src/goapp /app/
ENTRYPOINT ./goapp

Соберем и запустим снова:

docker build -t treeder/hello .
docker run --rm treeder/hello

Проверим размер:

6,35 МБ — намного лучше.

Что это означает? Что многоступенчатые сборки — отличная вещь. Ими стоит пользоваться практически в любом случае!

Источник: https://medium.com/southbridge/%D0%BC%D0%BD%D0%BE%D0%B3%D0%BE%D1%81%D1%82%D1%83%D0%BF%D0%B5%D0%BD%D1%87%D0%B0%D1%82%D0%B0%D1%8F-%D1%81%D0%B1%D0%BE%D1%80%D0%BA%D0%B0-docker-%D0%BE%D0%B1%D1%80%D0%B0%D0%B7%D0%BE%D0%B2-8b766785f75a

Was this helpful?

0 / 0