Pull to refresh

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

Reading time2 min
Views77K
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 .

Ссылки:

Tags:
Hubs:
+25
Comments11

Articles