Docker начиная с версии 17.05 и выше стал поддерживать многоэтапные сборки (multi-stage builds). С удивлением обнаружил, что никто еще не написал об этом на хабре. Поэтому давайте исправим этот пробел.

Изменения будут особенно полезны тем, кто собирает образы (images) на базе уже существующих и кому необходимо поддерживать их минимальный размер.

Каждый, кто собирал docker images знает, что практически каждая инструкция в Dockerfile добавляет отдельный слой и вам необходимо очистить этот слой от всех лишних артефактов, перед тем как добавить новый слой. Поэтому чтобы создать действительно эффективный Dockerfile раньше вам традиционно предлагали использовать скрипты и другую логику, чтобы поддерживать минимально возможный размер слоя. Обычной практикой было использовалось несколько Dockerfile в зависимости от целей сборки образа — один файл для DEVELOPMENT с определенным набором средства для отладки, профайлинга и всего остального и другой образ, гораздо меньшего размера для развертывания приложение на STAGING или PRODUCTION, с набором компонентов, необходимых для работы приложения.

Допустим у нас есть простой “hello world” HTTP-server, который нужно собраться и запустить тесты, а после собрать минимальные docker образ, которые содержит только исполняемые файлы.

Пример можно взять отсюда

Минимальный Dockerfile у нас будет выглядеть вот так.

Dockerfile:

FROM golang:latest
COPY . .
RUN go test && go build ./src/main.go

Давайте соберем и запустим образ:

docker image build -t hello_world:build .

Если посмотреть метаданные образа: docker image inspect hello_world:build то видно, что он состоит из 6 отдельный слоев и занимает около 800MB. И это только Hello World, а какой размер может быть у реального приложение можно только представить. Поэтому для PRODUCTION уже имеет смысл собрать образ только из исполняемых файлов.

В результате вы должны запустить вот такую последовательность команд:

Запустить базовый контейнер:

docker container run -it --name hello_world hello_world:build

Создать новый контейнер на базе уже существующего и скопировать бинарные файлы:

docker create --name extract hello_world:build
mkdir ./Production/
docker cp extract:/go/main ./Production/main
docker rm -f extract
docker rm -f hello_world

Создать PRODUCTION контейнер, содержащий только необходимые файлы для работы приложения:

docker build --no-cache -t hello_world:latest ./Production/
rm ./Production

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

Так вот много-этапные (multi-stage builds) сборки позволяют значительно упростить этот процесс и описать его внутри Dockerfile. Каждая инструкция FROM может использовать индивидуальный базовый образ и каждая из них начинает новую стадию сборки docker образа. Но основное преимущество, что вы можете копировать необходимые артефакты из одной стадии в другую. В результате все вышеперечисленные шаги могут быть описаны вот так
Dockerfile:

FROM golang:latest as build
COPY . .
RUN go build ./src/main.go

FROM alpine:latest as production
COPY --from=build /go/main .
CMD ["./main"]

И все что вам остается, это выполнить команду:

docker image build -t hello_world:latest .

Note: отдельно стоит добавить, что к предыдущием стадия вы можете обращаться как по алиасу указанному в инструкции FROM golang:latest as build — как в примере выше COPY --from=build /go/main ., так и по индексу COPY --from=0 /go/main .

Ссылки:

Комментарии (8)


  1. zelenin
    23.02.2018 23:09

    1. BOOTLOADER Автор
      24.02.2018 00:16

      Спасибо. Немного другим язык, но об одном и том же.


  1. VolCh
    23.02.2018 23:10

    COPY --from позволяет указывать не только стейджы из текущего докерфайла, но любой образ доступный для FROM. Очень полезно бывает, когда из какого-то официального образа нужен по сути только один файл или каталог.


    1. BOOTLOADER Автор
      24.02.2018 21:57

      Спасибо. На знал этого.


  1. dmitry_ch
    24.02.2018 07:41
    -1

    Тема старая, страдательный. Однако тот же dapp мне как-то больше нравится: собирает быстрее, и не завязан на самую свежую версию докеры (а смена не всегда обоснована) — https://habrahabr.ru/company/flant/blog/333682/


    Докеры молодцы, что лучше поздно, но сделали.


    1. BOOTLOADER Автор
      24.02.2018 21:58

      Не знаком с Dapp к сожалению. Спасибо за ссылку — посмотрю. А в чем его отличия может кратко сказать, и в чем удобство?


      1. dmitry_ch
        25.02.2018 11:32

        Во-первых, посоветовал бы флантовское выступление посмотреть, там и подходы, и решения описаны. Во-вторых, dapp сделан когда ещё докер об этом не думал. В-третьих, если у вас проект на старом докером, то переход на новый может и не быть у вас в планах — и вот эта описанная вами милая фича просто недоступна.


        Dapp делался для скорости сборки образов и для уменьшения их размера. Грубо, если вы изменили исходники на 1 байт (и исходник — это код на том же ror), то размер образа не должен прыгать на 100 мб, а образ не должно собираться 5 минут.


        1. hippoage
          26.02.2018 01:15
          +1

          Dapp довольно жуткий, поэтому массово и не взелетел. С помощью примитивных скриптов и какого-нибудь проекта docker-squash (если не хочется собирать образ новым докером) то же самое отлично решается.