Home
Unchained
Engineering Blog

Docker Bake and Chainguard Images

Adrian Mouat, Staff DevRel Engineer

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.

Share

Ready to Lock Down Your Supply Chain?

Talk to our customer obsessed, community-driven team.

Get Started