Building Compose projects with Bake

This guide explores how you can use Bake to build images for Docker Compose projects with multiple services.

Docker Buildx Bake is a build orchestration tool that enables declarative configuration for your builds, much like Docker Compose does for defining runtime stacks. For projects where Docker Compose is used to spin up services for local development, Bake offers a way of seamlessly extending the project with a production-ready build configuration.

Prerequisites

This guide assumes that you're familiar with

Orientation

This guide will use the dvdksn/example-voting-app repository as an example of a monorepo using Docker Compose that can be extended with Bake.

$ git clone https://github.com/dvdksn/example-voting-app.git
$ cd example-voting-app

This repository uses Docker Compose to define the runtime configurations for running the application, in the compose.yaml file. This app consists of the following services:

ServiceDescription
voteA front-end web app in Python which lets you vote between two options.
resultA Node.js web app which shows the results of the voting in real time.
workerA .NET worker which consumes votes and stores them in the database.
dbA Postgres database backed by a Docker volume.
redisA Redis instance which collects new votes.
seedA utility container that seeds the database with mock data.

The vote, result, and worker services are built from code in this repository, whereas db and redis use pre-existing Postgres and Redis images from Docker Hub. The seed service is a utility that invokes requests against the front-end service to populate the database, for testing purposes.

Build with Compose

When you spin up a Docker Compose project, any services that define the build property are automatically built before the service is started. Here's the build configuration for the vote service in the example repository:

compose.yaml
services:
  vote:
    build:
      context: ./vote # Build context
      target: dev # Dockerfile stage

The vote, result, and worker services all have a build configuration specified. Running docker compose up will trigger a build of these services.

Did you know that you can also use Compose just to build the service images? The docker compose build command lets you invoke a build using the build configuration specified in the Compose file. For example, to build the vote service with this configuration, run:

$ docker compose build vote

Omit the service name to build all services at once:

$ docker compose build

The docker compose build command is useful when you only need to build images without running services.

The Compose file format supports a number of properties for defining your build's configuration. For example, to specify the tag name for the images, set the image property on the service.

services:
  vote:
    image: username/vote
    build:
      context: ./vote
      target: dev
    #...

  result:
    image: username/result
    build:
      context: ./result
    #...

  worker:
    image: username/worker
    build:
      context: ./worker
    #...

Running docker compose build creates three service images with fully qualified image names that you can push to Docker Hub.

The build property supports a wide range of options for configuring builds. However, building production-grade images are often different from images used in local development. To avoid cluttering your Compose file with build configurations that might not be desirable for local builds, consider separating the production builds from the local builds by using Bake to build images for release. This approach separates concerns: using Compose for local development and Bake for production-ready builds, while still reusing service definitions and fundamental build configurations.

Build with Bake

Like Compose, Bake parses the build definition for a project from a configuration file. Bake supports HashiCorp Configuration Language (HCL), JSON, and the Docker Compose YAML format. When you use Bake with multiple files, it will find and merge all of the applicable configuration files into one unified build configuration. The build options defined in your Compose file are extended, or in some cases overridden, by options specified in the Bake file.

The following section explores how you can use Bake to extend the build options defined in your Compose file for production.

View the build configuration

Bake automatically creates a build configuration from the build properties of your services. Use the --print flag for Bake to view the build configuration for a given Compose file. This flag evaluates the build configuration and outputs the build definition in JSON format.

$ docker buildx bake --print

The JSON-formatted output shows the group that would be executed, and all the targets of that group. A group is a collection of builds, and a target represents a single build.

{
  "group": {
    "default": {
      "targets": [
        "vote",
        "result",
        "worker",
        "seed"
      ]
    }
  },
  "target": {
    "result": {
      "context": "result",
      "dockerfile": "Dockerfile",
    },
    "seed": {
      "context": "seed-data",
      "dockerfile": "Dockerfile",
    },
    "vote": {
      "context": "vote",
      "dockerfile": "Dockerfile",
      "target": "dev",
    },
    "worker": {
      "context": "worker",
      "dockerfile": "Dockerfile",
    }
  }
}

As you can see, Bake has created a default group that includes four targets:

  • seed
  • vote
  • result
  • worker

This group is created automatically from your Compose file; it includes all of your services containing a build configuration. To build this group of services with Bake, run:

$ docker buildx bake

Customize the build group

Start by redefining the default build group that Bake executes. The current default group includes a seed target — a Compose service used solely to populate the database with mock data. Since this target doesn't produce a production image, it doesn’t need to be included in the build group.

To customize the build configuration that Bake uses, create a new file at the root of the repository, alongside your compose.yaml file, named docker-bake.hcl.

$ touch docker-bake.hcl

Open the Bake file and add the following configuration:

docker-bake.hcl
group "default" {
  targets = ["vote", "result", "worker"]
}

Save the file and print your Bake definition again.

$ docker buildx bake --print

The JSON output shows that the default group only includes the targets you care about.

{
  "group": {
    "default": {
      "targets": ["vote", "result", "worker"]
    }
  },
  "target": {
    "result": {
      "context": "result",
      "dockerfile": "Dockerfile",
      "tags": ["username/result"]
    },
    "vote": {
      "context": "vote",
      "dockerfile": "Dockerfile",
      "tags": ["username/vote"],
      "target": "dev"
    },
    "worker": {
      "context": "worker",
      "dockerfile": "Dockerfile",
      "tags": ["username/worker"]
    }
  }
}

Here, the build configuration for each target (context, tags, etc.) is picked up from the compose.yaml file. The group is defined by the docker-bake.hcl file.

Customize targets

The Compose file currently defines the dev stage as the build target for the vote service. That's appropriate for the image that you would run in local development, because the dev stage includes additional development dependencies and configurations. For the production image, however, you'll want to target the final image instead.

To modify the target stage used by the vote service, add the following configuration to the Bake file:

target "vote" {
  target = "final"
}

This overrides the target property specified in the Compose file with a different value when you run the build with Bake. The other build options in the Compose file (tag, context) remain unmodified. You can verify by inspecting the build configuration for the vote target with docker buildx bake --print vote:

{
  "group": {
    "default": {
      "targets": ["vote"]
    }
  },
  "target": {
    "vote": {
      "context": "vote",
      "dockerfile": "Dockerfile",
      "tags": ["username/vote"],
      "target": "final"
    }
  }
}

Additional build features

Production-grade builds often have different characteristics from development builds. Here are a few examples of things you might want to add for production images.

Multi-platform
For local development, you only need to build images for your local platform, since those images are just going to run on your machine. But for images that are pushed to a registry, it's often a good idea to build for multiple platforms, arm64 and amd64 in particular.
Attestations
Attestations are manifests attached to the image that describe how the image was created and what components it contains. Attaching attestations to your images helps ensure that your images follow software supply chain best practices.
Annotations
Annotations provide descriptive metadata for images. Use annotations to record arbitrary information and attach it to your image, which helps consumers and tools understand the origin, contents, and how to use the image.

Tip

Why not just define these additional build options in the Compose file directly?

The build property in the Compose file format does not support all build features. Additionally, some features, like multi-platform builds, can drastically increase the time it takes to build a service. For local development, you're better off keeping your build step simple and fast, saving the bells and whistles for release builds.

To add these properties to the images you build with Bake, update the Bake file as follows:

group "default" {
  targets = ["vote", "result", "worker"]
}

target "_common" {
  annotations = ["org.opencontainers.image.authors=username"]
  platforms = ["linux/amd64", "linux/arm64"]
  attest = [
    "type=provenance,mode=max",
    "type=sbom"
  ]
}

target "vote" {
  inherits = ["_common"]
  target = "final"
}

target "result" {
  inherits = ["_common"]
}

target "worker" {
  inherits = ["_common"]
}

This defines a new _common target that defines reusable build configuration for adding multi-platform support, annotations, and attestations to your images. The reusable target is inherited by the build targets.

With these changes, building the project with Bake produces three sets of multi-platform images for the linux/amd64 and linux/arm64 architectures. Each image is decorated with an author annotation, and both SBOM and provenance attestation records.

Conclusions

The pattern demonstrated in this guide provides a useful approach for managing production-ready Docker images in projects using Docker Compose. Using Bake gives you access to all the powerful features of Buildx and BuildKit, and also helps separate your development and build configuration in a reasonable way.

Further reading

For more information about how to use Bake, check out these resources: