Многоэтапные (multi-stage builds) сборки в Docker

  • Tutorial
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 .

Ссылки:

  • +25
  • 8,3k
  • 9
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 9
    • 0
      Спасибо. Немного другим язык, но об одном и том же.
    • +5
      COPY --from позволяет указывать не только стейджы из текущего докерфайла, но любой образ доступный для FROM. Очень полезно бывает, когда из какого-то официального образа нужен по сути только один файл или каталог.
    • 0

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


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

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

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


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

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

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое