Introduction

The command docker build is the standard for creating docker images. However, as projects evolve and the image creation step becomes more intricate, the command-line invocations grow into unwieldy strings of flags and arguments. Managing these complex commands consistently across teams can become a significant challenge, often leading to errors and inconsistencies in the build process. Ultimately, they are shell commands.

This is where Docker Bake steps in. Built directly into Docker Buildx, Bake offers a more structured and maintainable approach to defining and executing Docker builds. Instead of relying on lengthy CLI commands, Bake allows you to define your build configurations in declarative files. Think of Bake as your organized build recipe book, which you can pass down to your teams.

Docker build commands

Here’s an example workflow. When a developer merges a PR into the main branch, the main pipeline kicks off. After all security checks and automated tests are done, the Docker build process starts before publishing the image to the repository. For a single build, four images are created: two images for the AMD64 architecture (one with the latest tag and one with the tag containing the last commit’s hash) and two corresponding images for the ARM64 architecture.

Dockerfile:

FROM node:${NODE_VERSION}-alpine
WORKDIR /app
COPY package-lock.json ./
COPY package.json ./
RUN npm install
COPY index.js .
EXPOSE 8080
CMD ["npm", "start"]

Docker build command for AMD64:

docker build \
  --pull \
  --no-cache \
  --build-arg NODE_VERSION=22 \
  --platform linux/amd64 \
  --tag myapp:latest-amd64 \
  --tag myapp:${COMMIT_SHA}-amd64 \
  --label maintainer=Subhransu-De \
  -f Dockerfile \
  .

output:

$ docker images
REPOSITORY   TAG              IMAGE ID       CREATED          SIZE
myapp        19d6ca8-amd64    247d8c26eb79   1 minutes ago    167MB
myapp        latest-amd64     247d8c26eb79   1 minutes ago    167MB

Docker build command for ARM64:

docker build \
  --pull \
  --no-cache \
  --build-arg NODE_VERSION=22 \
  --platform linux/arm64 \
  --tag myapp:latest-arm64 \
  --tag myapp:${COMMIT_SHA}-arm64 \
  --label maintainer=Subhransu-De \
  -f Dockerfile \
  .

output:

$ docker images
REPOSITORY   TAG              IMAGE ID       CREATED          SIZE
myapp        19d6ca8-arm64    d9e23f7431d8   1 minutes ago    164MB
myapp        latest-arm64     d9e23f7431d8   1 minutes ago    164MB

Dissecting the build command

  • --platform: This flag instructs Docker to build images for a specific architecture.
  • --pull: Pull the base image even if it exists locally.
  • --no-cache: This flag forces a rebuild from scratch, ignoring any cached layers.
  • --tag: For each platform, two tags are created.

Docker Bake version

Now, here is the Docker Bake version of these two commands in one declarative Bake file.

docker-bake.hcl:

variable "APP_NAME" {
  default     = "myapp"
  description = "The name of the application."
}

variable "NODE_VERSION" {
  default     = "22"
  description = "The version of Node.js to use."
}

variable "TAG" {
  default     = "latest"
  description = "The tag for the application."
}

variable "COMMIT_SHA" {
  default     = ""
  description = "The last commit's HASH. CMD: git rev-parse --short HEAD"
}

group "default" {
  targets = ["build"]
}

target "base" {
  context    = "."
  dockerfile = "Dockerfile"
  args = {
    NODE_VERSION = NODE_VERSION
  }
  pull     = true
  no-cache = true
  labels = {
    maintainer = "Subhransu-De"
  }
  output = ["type=docker"]
}

target "build" {
  inherits = ["base"]
  matrix = {
    tgt = ["linux/amd64", "linux/arm64"]
  }
  name      = "${APP_NAME}-${replace(tgt, "/", "-")}"
  platforms = [tgt]
  tags = [
    "${APP_NAME}:${TAG}-${replace(tgt, "linux/", "")}",
    "${APP_NAME}:${COMMIT_SHA}-${replace(tgt, "linux/", "")}"
  ]
}

Let’s dissect this docker-bake.hcl file.

Variables

These are values that are available during the build process. They can have default values defined within the Bake file, which can be overridden by setting environment variables with the same name. Here are the variables defined in the example:

  • APP_NAME: Stores name of the application.
  • TAG: Stores the static tag name, “latest”.
  • NODE_VERSION: As seen in the Dockerfile, a build argument specifies the Node.js version to be installed in the container. This variable stores the node version to be sent during the build process.

If the Dockerfile contains a default value for this argument, it will be overridden by the value defined in the Bake file (or an environment variable).

  • COMMIT_SHA: This variable stores the value of the last commit’s hash. It is used to create a specific tag for the Docker image, in addition to the latest tag. The default value of this variable is overridden by defining an environment variable during the build process. For example:
export COMMIT_SHA=$(git rev-parse --short HEAD); docker buildx bake

Targets

These are used to define individual build processes or configurations within a Bake file. A base target can be established to contain common settings applicable to all builds. This base target can then be easily inherited by other targets using the inherits = ["base"] directive.

The DevOps team can set up a base target as a template for the organization. Other teams can then extend this base target. This ensures consistency across the build process.

Matrix

This provides a powerful way to define and execute multiple variations of a target based on different parameters. In the example above, the matrix feature is used to specify two build variants: AMD64 and ARM64.

Groups

Allows you to organize and run multiple targets together. By default, Docker Bake runs the “default” group.

output:

$ export COMMIT_SHA=$(git rev-parse --short HEAD); docker buildx bake
...
$ docker images
REPOSITORY   TAG              IMAGE ID       CREATED          SIZE
myapp        19d6ca8-arm64    d9e23f7431d8   1 minutes ago    164MB
myapp        latest-arm64     d9e23f7431d8   1 minutes ago    164MB
myapp        19d6ca8-amd64    247d8c26eb79   1 minutes ago    167MB
myapp        latest-amd64     247d8c26eb79   1 minutes ago    167MB

Conclusion

Docker Bake offers a structured, declarative approach to defining Docker builds, significantly improving maintainability over complex docker build commands. Features like Inheritance and Matrix enhance its potential for teams managing multi-variant image builds. The matrix functionality efficiently avoids repetition, as demonstrated in building for both AMD64 and ARM64 architectures with consistent tagging. Its structured approach has the potential to make image management easier for big microservice systems.

Further readings