Docker Bake and Chainguard Images
If you're anything like me, you probably have horrendously long docker build commands that you manage via shell history. If you're more organised, you might have shell scripts or even Makefiles to manage your Docker build workflows. That is a better solution, but adds in an extra dependency that isn't always portable (and isn't the point of Docker to be portable?). Fortunately, there is a better solution, and it doesn't require any tooling beyond the Docker CLI: meet Bake. This post will cover what Bake is and how it can be used to simplify and automate workflows, looking in particular at multistage builds with Chainguard Images.
You can probably guess from the name that Bake is "make" for Docker builds. It's probably easiest to explain with an example. Here's a command I pulled out of my shell history:
docker build -t amouat/multi-plat-test --platform linux/arm64,linux/amd64 --push --no-cache -f cross.Dockerfile .
If we translate this into a Bake file it will look something like:
target "default" {
tags = ["amouat/multi-plat-test"]
platforms = [
"linux/amd64",
"linux/arm64",
]
output = ["type=registry"]
no-cache = true
dockerfile = "cross.Dockerfile"
context = "."
}
If this file is saved as docker-bake.hcl
, you can then run docker buildx bake
to execute it. It will have the same effect as the above docker build
command, but now you don't need to remember a dozen different flags. This isn't a great example, but the point is that the most basic functionality of Docker Bake is to codify Docker builds, which can be done quickly and easily.
Complete Example
Bake is particularly useful with Chainguard Images, as you'll typically have a multi-stage workflow and may well have different images for development and production. We can use Bake to create separate targets for these which inherit from each other to reduce duplication and simplify changing contexts. I've added a Bake file to this example repo, which gives a complete example:
variable "REPO" {
default = "amouat/bake"
}
target "base" {
description = "Basic development build"
context = "."
dockerfile = "Dockerfile"
tags = ["${REPO}:base"]
output = ["type=docker"]
pull = true
}
group "default" {
targets = ["base"]
}
target "cross" {
description = "Builds multi-platform image using cross compilation"
inherits = ["base"]
dockerfile = "cross.Dockerfile"
tags = ["${REPO}:multi"]
platforms = [
"linux/amd64",
"linux/arm64",
]
}
target "push" {
description = "Pushes images to registry"
inherits = ["cross"]
attest = [
"type=provenance,mode=max",
"type=sbom"
]
output = ["type=registry"]
}
Before going further, it's worth mentioning that this Bake file is written using the HCL format. You can also use YAML or JSON, but there are some advantages to using HCL, including the ability to call some basic functions for string interpolation and other tasks.
The major difference with this example is that we've added some structure and support for multiple use cases. There are now multiple "targets" that can be built, and you can use the –list
command to get an overview:
docker buildx bake --list=targets
[+] Building 0.0s (1/1) FINISHED
=> [internal] load local bake definitions 0.0s
=> => reading docker-bake.hcl 673B / 673B 0.0s
TARGET DESCRIPTION
base Basic development build
cross Builds multi-platform image using cross compilation
default base
push Pushes images to registry
So we have a "base
" target that will create a single image for testing with, a "cross
" target that will build a multiplatform image, and a "push" target that will upload the multiplatform image to the registry. We've avoided a lot of duplication by using the "inherits
" syntax which allows a target to extend another one. The push target also takes a variable:
docker buildx bake --list=variables
[+] Building 0.0s (1/1) FINISHED
=> [internal] load local bake definitions 0.0s
=> => reading docker-bake.hcl 673B / 673B 0.0s
VARIABLE VALUE DESCRIPTION
REPO amouat/bake
To select a target just provide the name as an argument. For example, to call the cross
target:
docker buildx bake cross
…
And we can set the REPO
variable to push to a different registry:
REPO=amouat/new-repo docker buildx bake push
…
It's worth noting that I've also set some "attestations" in the push target – this adds an SBOM (Software Bill of Materials) and further build metadata that are useful when establishing the provenance of images.
Hopefully you can see how Bake can help simplify builds and make it easier for new developers to understand and leverage your build process. We've covered the basics of Bake, but there is a lot more that's possible.
Advanced Usage
There are some additional things that are worth being aware of:
Entitlements: Rather than allowing Bake to automatically perform potentially dangerous actions, Bake now requires the explicit passing of "entitlements" to perform actions such using host networking or accessing files outside the working directory.
Variable validation: It is now possible to add checks to ensure variables are correctly specified, e.g. not empty or the wrong length.
Output formats: Bake isn't just for images. You can also output other artifacts such as logs or binaries. One use case is to use the cross platform building features in Docker to easily create a binary for another platform.
Testing: You can instruct a build step to run a test suite and stop the build on any failures.
Functions: There is support for a range of functions for string manipulation and other common tasks.
Conclusion
If you're used to struggling with Docker build options or fighting with make files and shell scripts to create your Docker Images, it's definitely worth checking out Bake. Bake is used heavily inside Docker, so it will continue to be supported and developed.
Ready to Lock Down Your Supply Chain?
Talk to our customer obsessed, community-driven team.