Build Bake

docker buildx bake is the standard way to describe repeatable builds as code. Use it when command lines become long, when multiple images share settings, or when CI needs the same build targets as local development.

Bake can read docker-bake.hcl, Compose files, and JSON files. The HCL format is the most readable for build targets.

Why Bake

Use Bake when a project needs any of these:

  • Several images built from one repository.

  • Multi-platform release images.

  • Shared tags, labels, cache settings, SBOM, and provenance settings.

  • Local, test, and release build targets.

  • CI builds that should not duplicate shell scripts.

Basic file

Create docker-bake.hcl at the project root.

 1variable "REGISTRY" {
 2  default = "registry.example.com/team"
 3}
 4
 5variable "TAG" {
 6  default = "dev"
 7}
 8
 9group "default" {
10  targets = ["api"]
11}
12
13target "api" {
14  context = "."
15  dockerfile = "Dockerfile"
16  tags = ["${REGISTRY}/api:${TAG}"]
17}

List the targets.

1docker buildx bake --list

Build the default group.

1docker buildx bake

Override variables from the shell.

1TAG="$(git rev-parse --short HEAD)" docker buildx bake

Multi-platform release target

Keep local and release behavior separate. Local targets usually load one image into the local Docker image store. Release targets usually push multi-platform images to a registry.

 1variable "REGISTRY" {
 2  default = "registry.example.com/team"
 3}
 4
 5variable "TAG" {
 6  default = "dev"
 7}
 8
 9target "api-local" {
10  context = "."
11  dockerfile = "Dockerfile"
12  tags = ["api:local"]
13  output = ["type=docker"]
14}
15
16target "api-release" {
17  context = "."
18  dockerfile = "Dockerfile"
19  platforms = ["linux/amd64", "linux/arm64"]
20  tags = [
21    "${REGISTRY}/api:${TAG}",
22    "${REGISTRY}/api:latest"
23  ]
24  cache-from = ["type=registry,ref=${REGISTRY}/api:buildcache"]
25  cache-to = ["type=registry,ref=${REGISTRY}/api:buildcache,mode=max"]
26  attest = [
27    "type=sbom",
28    "type=provenance"
29  ]
30  output = ["type=registry"]
31}

Build locally.

1docker buildx bake api-local

Build and push the release image.

1TAG="$(git rev-parse --short HEAD)" docker buildx bake api-release

Multiple services

Bake shines when a repository contains several services.

 1variable "REGISTRY" {
 2  default = "registry.example.com/team"
 3}
 4
 5variable "TAG" {
 6  default = "dev"
 7}
 8
 9target "_common" {
10  platforms = ["linux/amd64", "linux/arm64"]
11  cache-from = ["type=registry,ref=${REGISTRY}/buildcache"]
12  cache-to = ["type=registry,ref=${REGISTRY}/buildcache,mode=max"]
13}
14
15target "api" {
16  inherits = ["_common"]
17  context = "./services/api"
18  tags = ["${REGISTRY}/api:${TAG}"]
19}
20
21target "worker" {
22  inherits = ["_common"]
23  context = "./services/worker"
24  tags = ["${REGISTRY}/worker:${TAG}"]
25}
26
27group "release" {
28  targets = ["api", "worker"]
29}

Run the group.

1TAG="$(git rev-parse --short HEAD)" docker buildx bake release --push

CI pattern

In CI, keep the policy in docker-bake.hcl and let the workflow supply variables.

1set -euo pipefail
2
3docker buildx create --name ci-builder --driver docker-container --use
4docker buildx inspect --bootstrap
5docker buildx bake --list
6TAG="${GIT_SHA}" docker buildx bake release --push

Use the same target names locally and in CI. The less custom shell logic in CI, the easier it is to reproduce a release build.

Practical rules

  • Put shared cache, platform, label, and attestation settings in inherited targets.

  • Keep local targets separate from release targets.

  • Prefer registry output for multi-platform images.

  • Use immutable commit tags for releases.

  • Use latest only as a convenience pointer, not as the deployment record.

  • Keep Compose responsible for running services and Bake responsible for building images.

References