Многоступенчатые сборки
Многоэтапные сборки полезны всем, кто пытался оптимизировать Docker-файлы, сохраняя их легкость чтения и сопровождения.
Примечание
Признательность
Особая благодарность Алекс Эллис за разрешение использовать его сообщение в блоге Шаблон Builder против многоэтапных сборок в Docker в качестве основы для примеры далее.
Перед многоступенчатым построением
Один из самых сложных моментов при создании образов — это уменьшение размера образа. Каждая оператор RUN
, COPY
и ADD
в Dockerfile добавляет слой к образу, и вам нужно не забыть очистить все ненужные артефакты перед переходом к следующему слою. Чтобы написать действительно эффективный Dockerfile, традиционно необходимо использовать трюки оболочки и другую логику, чтобы слои были как можно меньше и чтобы каждый слой имел только те артефакты, которые ему нужны от предыдущего слоя, и ничего больше.
На самом деле было очень распространено использование одного Dockerfile для разработки (который содержал все необходимое для создания вашего приложения) и уменьшенного Dockerfile для производства, который содержал только ваше приложение и то, что было необходимо для его запуска. Такой подход называют «паттерном сборщика». Ведение двух Docker-файлов не является идеальным.
Вот пример build.Dockerfile
и Dockerfile
, которые соответствуют приведенному выше шаблону строителя:
build.Dockerfile
:
# syntax=docker/dockerfile:1
FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY app.go ./
RUN go get -d -v golang.org/x/net/html \
&& CGO_ENABLED=0 go build -a -installsuffix cgo -o app .
Обратите внимание, что данный пример также искусственно сжимает две команды RUN
вместе с помощью оператора Bash &&
, чтобы избежать создания дополнительного слоя в образы. Это чревато сбоями и трудно поддерживать. Легко вставляет другую команду и забыть продолжить строку, используя, например, символ \
.
Dockerfile
:
# syntax=docker/dockerfile:1
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app ./
CMD ["./app"]
build.sh
:
#!/bin/sh
echo Building alexellis2/href-counter:build
docker build -t alexellis2/href-counter:build . -f build.Dockerfile
docker container create --name extract alexellis2/href-counter:build
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app
docker container rm -f extract
echo Building alexellis2/href-counter:latest
docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app
Когда вы запускаете сценарий build.sh
, ему нужно сгенерировать первый образ, создать из него контейнер для копирования артефакта, а затем создает второй образ. Оба образа занимают место в системе, а артефакт app
все ещё находится на локальном диске.
Многоступенчатые сборки значительно упрощают эту ситуацию!
Используйте многоступенчатые сборки
При многоэтапной сборке вы используете несколько инструкций FROM
в своём Dockerfile. Каждая оператор FROM
может использовать разную базу, и каждая из них начинает новый этап сборки. Вы можете выборочно копировать артефакты с одного этапа на другой, оставляя в конечном образе все, что вам не нужно. Чтобы показывает, как это работает, давайте адаптируем инструкцию Dockerfile
из предыдущего раздела для использования многоэтапной сборки.
# syntax=docker/dockerfile:1
FROM golang:1.16
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 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
.
$ docker build -t alexellis2/href-counter:latest .
Конечным результатом является то же самое крошечное производственное образ, что и раньше, со значительным снижением сложности. Вам не нужно создавать никаких промежуточных образов, и вам вообще не нужно извлекать какие-либо артефакты в локальную систему.
Как это работает? Вторая оператор FROM
запускает новый этап сборки, основой которого является образ alpine:latest
. Строка COPY --from=0
копирует только собранный артефакт с предыдущего этапа в данный новый этап. Go SDK и любые промежуточные артефакты остаются позади и не сохраняются в конечном образе.
Назовите этапы сборки
По умолчанию этапы не имеют имён, и вы обращаетесь к ним по их целочисленному номеру, начиная с 0 для первой инструкции FROM
. Однако вы можете присвоить имя своим этапам, добавив AS <NAME>
к инструкции FROM
. Данный пример улучшает предыдущий, называя этапы и используя имя в инструкции COPY
. Это означает, что даже если инструкции в вашем Dockerfile впоследствии будут переупорядочены, оператор COPY
не сломается.
# syntax=docker/dockerfile:1
FROM golang:1.16 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 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"]
Остановка на определенном этапе сборки
При сборке образа не обязательно собирать весь Dockerfile, включая все этапы. Вы можете указывает целевой этап сборки. Следующая команда предполагает, что вы используете предыдущий Dockerfile
, но останавливаетесь на этапе с именем builder
:
$ docker build --target builder -t alexellis2/href-counter:latest .
Несколько сценариев, в которых это может быть очень полезно:
Отладка определённого этапа сборки
Использование каскада
debug
со всеми включенными отладочными символами или инструментами и обедненного каскадаproduction
Использование этапа
testing
, на котором ваше приложение заполняется тестовыми данными, а для производства используется другой этап, использующий реальные данные.
Используйте внешнее образ в качестве «сцены»
При использовании многоэтапных сборок вы не ограничены копированием из этапов, созданных ранее в вашем Dockerfile. Вы можете использовать инструкцию COPY --from
для копирования из отдельного образа, используя имя локального образа, тег, доступный локально или в реестре Docker, или идентификатор тега. При необходимости клиент Docker извлекает образ и копирует артефакт оттуда. Синтаксис следующий:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
Используйте предыдущий этап в качестве нового этапа
Вы можете продолжить предыдущий этап, сославшись на него при использовании директивы FROM
. Например:
# syntax=docker/dockerfile:1
FROM alpine:latest AS builder
RUN apk --no-cache add build-base
FROM builder AS build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp
FROM builder AS build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp
Совместимость версий
Синтаксис многоэтапной сборки был введен в Docker Engine 17.05.
Различия между унаследованным конструктором и BuildKit
Старый сборщик Docker Engine обрабатывает все этапы Dockerfile, ведущие к выбранному --target
. Он будет собирать этап, даже если выбранная цель не зависит от этого этапа.
BuildKit строит только те этапы, от которых зависит целевой этап.
Например, если дать следующий Dockerfile:
# syntax=docker/dockerfile:1
FROM ubuntu AS base
RUN echo "base"
FROM base AS stage1
RUN echo "stage1"
FROM base AS stage2
RUN echo "stage2"
В BuildKit включён сборка цели stage2
в этом Dockerfile означает, что обрабатываются только base
и stage2
. Зависимость от stage1
отсутствует, поэтому она пропускается.
$ DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile --target stage2 .
[+] Building 0.4s (7/7) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 36B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> CACHED [base 1/2] FROM docker.io/library/ubuntu 0.0s
=> [base 2/2] RUN echo "base" 0.1s
=> [stage2 1/1] RUN echo "stage2" 0.2s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:f55003b607cef37614f607f0728e6fd4d113a4bf7ef12210da338c716f2cfd15 0.0s
С другой стороны, сборка той же цели без BuildKit приводит к тому, что все этапы обрабатываются:
$ DOCKER_BUILDKIT=0 docker build --no-cache -f Dockerfile --target stage2 .
Sending build context to Docker daemon 219.1kB
Step 1/6 : FROM ubuntu AS base
---> a7870fd478f4
Step 2/6 : RUN echo "base"
---> Running in e850d0e42eca
base
Removing intermediate container e850d0e42eca
---> d9f69f23cac8
Step 3/6 : FROM base AS stage1
---> d9f69f23cac8
Step 4/6 : RUN echo "stage1"
---> Running in 758ba6c1a9a3
stage1
Removing intermediate container 758ba6c1a9a3
---> 396baa55b8c3
Step 5/6 : FROM base AS stage2
---> d9f69f23cac8
Step 6/6 : RUN echo "stage2"
---> Running in bbc025b93175
stage2
Removing intermediate container bbc025b93175
---> 09fc3770a9c4
Successfully built 09fc3770a9c4
stage1
выполняется, когда BuildKit отключён, даже если stage2
не зависит от него.