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
- Docker Compose
- Multi-stage builds
- Multi-platform builds
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:
Service | Description |
---|---|
vote | A front-end web app in Python which lets you vote between two options. |
result | A Node.js web app which shows the results of the voting in real time. |
worker | A .NET worker which consumes votes and stores them in the database. |
db | A Postgres database backed by a Docker volume. |
redis | A Redis instance which collects new votes. |
seed | A 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:
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:
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: