BuildKit’s named build contexts let you inject arbitrary files and directories into your build process, and they’re way more powerful than just copying files in.
Here’s a quick look at how it works. Imagine you have a Dockerfile that needs some configuration files that aren’t in the same directory as your Dockerfile.
# syntax=docker/dockerfile:1
FROM alpine:latest
COPY --from=my-config-context /etc/app/config.yaml /app/config.yaml
RUN cat /app/config.yaml
And your project structure looks like this:
.
├── Dockerfile
└── configs
└── app
└── config.yaml
Normally, COPY would expect configs/app/config.yaml to be relative to the build context (which is usually .). But we want to treat configs as its own separate context.
This is where named build contexts shine. You define them on the docker build command line:
docker build \
--build-arg MY_CONFIG_PATH=./configs \
--mount type=cache,target=/app/cache \
-t my-app \
.
Wait, that’s not quite right. The COPY --from= syntax is for multi-stage builds. For injecting arbitrary files during the build, you use COPY --from=<context-name>.
Let’s refine that. The COPY --from= syntax in a Dockerfile refers to a previous build stage by its name or index, not to external build contexts directly in that way.
To use named build contexts as inputs to your build, you define them when you invoke BuildKit, typically via the docker buildx build command.
Let’s say you want to bring in a secrets.txt file that’s in a separate directory, ~/my-secrets, and make it available in your build.
First, you’d define this context when you run docker buildx build:
docker buildx build \
--build-arg SECRET_FILE_PATH=/app/secrets/secrets.txt \
--mount type=bind,source=~/my-secrets,target=/app/secrets \
-t my-app \
.
This command uses buildx (which leverages BuildKit). The --mount type=bind,source=~/my-secrets,target=/app/secrets tells BuildKit to mount the directory ~/my-secrets from your host machine into the build container at /app/secrets.
Now, your Dockerfile can reference files within that mounted directory:
# syntax=docker/dockerfile:1
FROM alpine:latest
ARG SECRET_FILE_PATH
RUN echo "Reading secrets from ${SECRET_FILE_PATH}" \
&& cat ${SECRET_FILE_PATH}
When you run the docker buildx build command above, the cat command will output the contents of ~/my-secrets/secrets.txt.
The real power comes when you start thinking of these contexts as distinct, versionable entities. You can define them as remote Git repositories, tarballs, or even other Docker images.
Let’s say you have a common-configs repository that you want to include in multiple builds.
You can define it as a build context:
docker buildx build \
--context git[https://github.com/myorg/common-configs.git#main:configs] \
-t my-app-with-common-configs \
.
In this example, git[...] is a BuildKit context type. https://github.com/myorg/common-configs.git is the repository URL, #main specifies the branch, and :configs indicates that only the configs directory within that repository should be used as the context.
Your Dockerfile can then reference files from this injected context:
# syntax=docker/dockerfile:1
FROM alpine:latest
COPY --from=common-configs /etc/shared/nginx.conf /etc/nginx/nginx.conf
RUN nginx -t
Here, COPY --from=common-configs tells BuildKit to copy files from the context named common-configs (which we defined on the command line) into the build. The path /etc/shared/nginx.conf is relative to the root of the common-configs context that was injected.
This allows you to decouple common configuration or dependency management from your main application’s build. Each context can have its own versioning and lifecycle.
You can also use docker buildx create --use to set up named buildx builder instances that have specific contexts pre-configured.
docker buildx create --name mybuilder --use
docker buildx build \
--context git[https://github.com/myorg/common-configs.git#main:configs] \
-t my-app \
.
The COPY --from=<context-name> instruction in the Dockerfile is key here. It’s not copying from a previous stage in the Dockerfile itself, but rather from a named build context that was provided to the docker buildx build command. BuildKit internally maps these named contexts to the paths where they were injected.
The actual mechanism is that BuildKit creates a virtual filesystem for each context. When you use COPY --from=context-name, BuildKit accesses that virtual filesystem and streams the requested files into your build stage. This avoids needing to have all your source code or configurations in a single monolithic build context directory. It’s a form of content-addressable storage, where BuildKit can reuse identical context blobs across different builds or even different projects if they share the same content.
A surprising detail is that BuildKit doesn’t just copy files; it uses a content-addressable filesystem. This means if two different named contexts (or even different parts of the same context) contain identical files, BuildKit only stores that file content once. This can lead to significant disk space savings and faster builds, especially if you’re reusing common libraries or configuration files across many projects.
The next logical step is to explore how to define custom context types or integrate BuildKit’s context mounting with CI/CD pipelines for automated dependency injection.