Dockerfiles now support Multiple Build Contexts

New releases Dockerfile 1.4 and Buildx v0.8+ allow you to define multiple assembly contexts. Now you can use files from different local directories as an assembly. Let’s see what is the use of this and how to use it in the development of build processes.

Team docker build takes one positional argument, which is the path or URL to the build context. Often docker build . uses the current working directory as the build context.

Inside Dockerfile you can use commands COPY and ADDto copy files from the build context and make them available for subsequent build steps. In BuildKit, we also added mount directories at build time with RUN --mountwhich allows you to access files directly without copying, to increase performance.

Content:

Conquering complex assemblies

As assemblies become more complex, being able to access files from only one place becomes very restrictive. That’s why we added multi-stage builds so you can copy files from different parts Dockerfileadding a flag --from and specifying the path to the name of another stage Dockerfile or remote image.

The new named assembly contexts are a continuation of this pattern. You can now define additional build contexts when executing a build command. Give them a name and you can refer to them internally Dockerfile the same as with build stages.

Additional contexts can be defined with a new flag --build-context [name]=[value]. The key component defines the name of the assembly context. The value options are:

on the side Dockerfile you can refer to one or another assembly context in all commands that take a “from” parameter. This is how it might look:

# syntax=docker/dockerfile:1.4
FROM [name]
COPY --from=[name] ...
RUN --mount=from=[name] …

Meaning [name] maps to the following order of precedence:

If a --from is not passed, then the files will be loaded from the main assembly context.

Example #1: Image Pin

Let’s start with an example of how to use build contexts to pin a version specific image used in Dockerfile.

This can come in handy in many cases. For example, with new function BuildInfo you can use all build sources and run the build with the same dependencies as the previous build, even if the image tags have been updated.

docker buildx imagetools inspect --format '{{json .BuildInfo}}' moby/buildkit
"sources": [
      {
        "type": "docker-image",
        "ref": "docker.io/library/alpine:3.15",
        "pin": "sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300"
      },
docker buildx build --build-context alpine:3.15=docker-image://alpine:3.15@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300 .

Если Dockerfile использует alpine:3.15, то даже с обновлённой версией реестра новая сборка всё равно будет использовать тот же самый образ, что и предыдущая.

А вот другой пример. Вы можете просто попробовать использовать другой образ или другую версию для отладки или создания вашего образа. Общей закономерностью может быть то, что вы ещё не зарелизили новый образ, и пока что он только в test или stage registry. Давайте представим, что создали своё приложение и запушили его в staging-репозиторий, но теперь хотите использовать его в других сборках, которые обычно используют release-образ.

docker buildx build --build-context myorg/myapp=docker-image://staging.myorg.com/registry/myapp .

Предыдущие примеры можно также рассматривать как способ создания алиаса для образа.

Пример №2: несколько проектов

Наверное, самый востребованный юзкейс применения именованных контекстов — возможность использования нескольких локальных директорий.

Когда в проекте несколько компонентов, которые нужно собрать вместе, то может быть сложно загрузить их с помощью одного контекста сборки, где всё должно содержаться в одной директории. У вас сразу несколько проблем: к каждому компоненту будет получен доступ по его полному пути, и у вас может быть лишь один .dockerignore файл — а вы, возможно, хотите, чтобы у каждого компонента был свой личный Dockerfile.

Если у вашего проекта такая структура…

project
├── app1
│   ├── .dockerignore
│   ├── src
├── app2
│   ├── .dockerignore
│   ├── src
├── Dockerfile

…с таким Dockerfile…

#syntax=docker/dockerfile:1.4
FROM … AS build1
COPY –from=app1 . /src
 
FROM … AS build2
COPY –from=app2 . /src
 
FROM …
COPY –from=build1 /out/app1 /bin/
COPY –from=build2 /out/app2 /bin/

…то вы можете вызвать свою сборку с помощью docker buildx build –build-context app1=app1/src –build-context app2=app2/src .. Обе исходные директории предоставлены для Dockerfile по отдельности, и к ним можно получить доступ, используя соответствующие названия.

Еще это позволяет вам получить доступ к файлам, которые находятся вне исходного кода вашего основного проекта. Обычно по соображениям безопасности внутри Dockerfile вам нельзя иметь доступ к файлам вне вашего контекста сборки, используя родительский селектор ../. Теперь, пока все контексты сборки передаются напрямую от клиента, вы можете использовать --build-context othersource=../../path/to/other/project, чтобы обойти это ограничение.

Пример №3: замените удалённую зависимость локальной

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

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

Что-то вроде этого:

FROM golang AS helper
RUN apk add git
WORKDIR /src
ARG HELPERAPP_VERSION=1.0
RUN git clone https://github.com/someorg/helperapp.git && cd helperapp && git checkout $HELPERAPP_VERSION
WORKDIR /src/helperapp
RUN go build -o /out/helperapp .
 
FROM alpine
COPY –link –from=helper /out/helperapp /bin
COPY –link –from=build /out/myapp /bin

И это неплохо работает. Когда вы создаёте сборку, helperapp собирается напрямую из его исходного репозитория и копируется рядом с бинарным файлом вашего приложения. Когда вам понадобилось использовать другую версию, вы обращаетесь к аргументу сборки HELPERAPP_VERSION и указываете другое значение.

Но что, если вы разрабатываете приложение и нашли баг? Вы не уверены, является ли источником бага исходный код или приложение-помощник. Можно внести несколько локальных изменений в код helperapp, чтобы проанализировать ситуацию. Но с текущим кодом сначала придётся пушить изменения в Github, чтобы они могли быть перенесены в Dockerfile. Повторять этот процесс для каждого изменения кода очень больно и неэффективно.

Вместо этого заменим предыдущий код на:

FROM alpine AS helper-clone
RUN apk add git
WORKDIR /src
ARG HELPERAPP_VERSION=1.0
RUN git clone https://github.com/someorg/helperapp.git && cd helperapp && git checkout $HELPERAPP_VERSION
 
FROM scratch AS helper-src
COPY –from=helper-clone /src/helperapp /
 
FROM golang:alpine AS helper
WORKDIR helperapp
RUN –mount=target=.,from=helper-src go build -o /out/helperapp .
 
FROM alpine
COPY –link –from=helper /out/helperapp /bin
COPY –link –from=build /out/myapp /bin

По умолчанию этот Dockerfile ведёт себя так же, как предыдущий, клонируя репозиторий с GitHub. Но теперь мы добавили отдельный этап helper-src, который содержит исходный код для helperapp. И мы можем использовать новые фичи контекстов, чтобы при необходимости сделать замену на нашу локальную исходную директорию.

docker buildx build --build-context helper-src=../path/to/my/local/helper/checkout .

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

Именованные контексты в buildx bake

В дополнение к команде build, docker buildx также имеет команду, названную bake. Это высокоуровневая команда сборки, которая позволяет определить конфигурацию сборки в файле, вместо того чтобы каждый раз печатать в длинный список флагов для ваших команд сборки.

Также он позволяет запускать несколько сборок одновременно, определять переменные, обмениваться переменными между вашими отдельными конфигурациями сборок и тд. Он принимает конфигурации сборок в JSON, HCL и Docker Compose YAML файлах. Вы можете узнать больше об этом в документации Buildx.

Мы также добавили поддержку именованных контекстов в bake. Это полезно, потому что, если Dockerfile зависит от нескольких контекстов сборки, вы можете забыть, что нужно передавать эти значения с помощью флага --build-context каждый раз, когда вызываете команду сборки.

С помощью bake вы можете задать определение вашей цели. Например:

hcl
target “binary” {
  contexts = {
    app1 = “app1/src”
    app2 = “app2/src”
  }
}

Теперь не нужно каждый раз вспоминать об использовании флага --build-context с правильными путями. Просто вызовите docker buildx bake binary, и ваша сборка запустится с правильной конфигурацией. Конечно, использовать переменные Bake и остальное в этих полях можно и для более сложных случаев.

Вы можете также использовать этот паттерн для создания специальных bake-целей для отладки или тестирования образов в этапных репозиториях.

hcl
target “myapp” {
 …
}
 
target “myapp-stage” {
  inherits = [“myapp”]
  contexts = { helperapp = "docker-image://staging.myorg.com/registry/myapp" } }

With a bake file like this one can call docker buildx bake myapp-stageto build the application with the same configuration defined for your target myapp. Except when your build uses an image helperappwhich she will download from the staging repository, instead of releasing what is written in Dockerfile.

Create build pipelines by referencing bake targets

In addition to images, Git, URLs, and local repositories, Bake files also support another definition that can be used as a named context.

You can set the source for a named context to another build target inside the Bake file. This way you can combine builds from multiple Dockerfiles that depend on each other and build them with a single command call.

Let’s imagine that we have two Dockerfiles:

# base.Dockerfile
FROM alpine
…
# Dockerfile
FROM baseapp
...

Usually collected first base.Dockerfilethen push it to the registry or leave it in the image store Docker. Then a second Dockerfile is built that downloads the image by name.

The problem is that if you’re using the Docker image store, it doesn’t currently support multi-platform local images. Using an external registry is not always convenient. And in both cases, some external changes can update the base image between two builds and cause the second build to use the wrong image. You need to run the build commands twice and sync them manually.

Instead, you can define a bake file using an assembly context prefixed with target:

target “base” {
  dockerfile = “base.Dockerfile”
  platforms = [“linux/amd64”, “linux/arm64”]
}
 
target “myapp” {
  contexts = {
    baseapp = “target:base”
  }
  platforms = [“linux/amd64”, “linux/arm64”]
}

Now you can build your application by simply running docker buildx bake myappto build both Dockerfiles and link them as needed. If you want to build both the base image and your application together, you can use docker buildx bake myapp base. Both of these targets are defined as multi-platform, and Buildx will take care of linking the respective single-platform images to each other.

Note that you should always consider using multi-stage builds first with the option --target in these conditions.

Having standalone Dockerfiles is an easier solution as it doesn’t require any extra parameters to be passed at build time. Use this method when you cannot combine multiple Dockerfiles and have to keep them separate.

See the new build context feature at DockerBuildx v0.8 releasewhich is part of the last docker desktop.

Similar Posts

Leave a Reply