Docker Bake: A Modern Approach to Container Building
Docker Bake offers a powerful, declarative approach to building container images with parallel processing, efficient caching, and inheritance.
Join the DZone community and get the full member experience.
Join For FreeThe traditional way of building Docker images using the docker build command is simple and straightforward, but when working with complex applications consisting of multiple components, this process can become tedious and error-prone. This is where Docker Bake comes in — a powerful and flexible tool for organizing multi-stage and parallel image building.
In this article, we'll look at the capabilities of Docker Bake, its advantages over the standard approach, and practical examples of its use for various development scenarios.
What Is Docker Bake?
Docker Bake is a BuildKit feature that allows you to organize and automate the Docker image-building process using configuration files.
The main advantages of Docker Bake:
- Declarative syntax. Instead of multiple commands in scripts, you describe the desired result in HCL (HashiCorp Configuration Language), JSON, or YAML (Docker Compose files).
- Parallel building. BuildKit automatically performs image building in parallel where possible.
- Cache reuse. Efficient use of cache between different builds.
- Grouping and targeted builds. Ability to define groups of images and build only the targets needed at the moment.
- Variables and inheritance. A powerful system of variables and property inheritance between build targets.
- CI/CD integration. Easily integrates into continuous integration and delivery pipelines.
Anatomy of a Bake File
Let's look at the main components of a bake file:
1. Variables
Variables allow you to define values that can be used in different parts of the configuration and easily redefined at runtime:
variable "TAG" {
default = "latest"
}
variable "DEBUG" {
default = "false"
}
Variables can be used in other parts of the configuration through string interpolation: ${TAG}
.
2. Groups
Groups allow you to combine multiple targets for simultaneous building:
group "default" {
targets = ["app", "api"]
}
group "backend" {
targets = ["api", "database"]
}
3. Targets
Targets are the main units of building, each defining one Docker image:
target "app" {
dockerfile = "Dockerfile.app"
context = "./app"
tags = ["myorg/app:${TAG}"]
args = {
DEBUG = "${DEBUG}"
}
platforms = ["linux/amd64", "linux/arm64"]
}
Main target parameters:
- dockerfile – path to the Dockerfile
- context – build context
- tags – tags for the image
- args – arguments to pass to the Dockerfile
- platforms – platforms for multi-platform building
- target – target for multi-stage building in Dockerfile
- output – where to output the build result
- cache-from and cache-to – cache settings
4. Inheritance
One of the most powerful features of Bake is the ability to inherit parameters:
target "base" {
context = "."
args = {
BASE_IMAGE = "node:16-alpine"
}
}
target "app" {
inherits = ["base"]
dockerfile = "app/Dockerfile"
tags = ["myapp/app:latest"]
}
The app target will inherit all parameters from the base and overwrite or supplement them with its own.
5. Functions
In HCL, you can define functions for more flexible configuration:
function "tag" {
params = [name, version]
result = ["${name}:${version}"]
}
target "app" {
tags = tag("myapp/app", "v1.0.0")
}
Installation and Setup
Docker Bake is part of BuildKit, a modern engine for building Docker images. Starting with Docker 23.0, BuildKit is enabled by default, so most users don't need additional configuration. However, if you're using an older version of Docker or want to make sure BuildKit is activated, follow the instructions below.
Checking the Docker Version
Make sure you have an up-to-date version of Docker (23.0 or higher). You can check the version with the command:
docker --version
If your Docker version is outdated, update it following the official documentation.
Activating BuildKit (for old Docker versions)
For Docker versions below 23.0, BuildKit needs to be activated manually. This can be done in one of the following ways:
-
Via environment variable:
Shellexport DOCKER_BUILDKIT=1
Plain TextIn the Docker configuration file: Edit the ~/.docker/config.json file and add the following parameters:
-
JSON
{ "features": { "buildkit": true } }
-
Via command line: When using the docker build or docker buildx bake command, you can explicitly specify the use of BuildKit:
ShellDOCKER_BUILDKIT=1 docker buildx bake
Installing Docker Buildx
Docker Buildx is an extension of the Docker CLI that provides additional capabilities for building images, including support for multi-platform building. Starting with Docker 20.10, Buildx is included with Docker, but for full functionality, it's recommended to ensure it's installed and activated.
Check Buildx Installation
docker buildx version
If Buildx is not installed, follow the instructions below.
Installing Buildx
- For Linux:
Shell
mkdir -p ~/.docker/cli-plugins curl -sSL https://github.com/docker/buildx/releases/latest/download/buildx-linux-amd64 -o ~/.docker/cli-plugins/docker-buildx chmod +x ~/.docker/cli-plugins/docker-buildx
- For macOS (using Homebrew):
Shell
brew install docker-buildx
Creating and Using a Buildx Builder
By default, Docker uses the built-in builder, but for full functionality, it's recommended to create a new builder:
docker buildx create --use --name my-builder
Check that the builder is active:
docker buildx ls
Docker Bake Basics
Configuration Files
Docker Bake uses configuration files that can be written in HCL (default), JSON, or YAML formats. Standard names for these files:
docker-bake.hcl
docker-bake.json
You can also use docker-compose.yml
with some extensions.
HCL File Structure
A typical Docker Bake configuration file has the following structure:
// Defining variables
variable "TAG" {
default = "latest"
}
// Defining groups
group "default" {
targets = ["app", "api"]
}
// Defining common settings
target "docker-metadata-action" {
tags = ["user/app:${TAG}"]
}
// Defining build targets
target "app" {
inherits = ["docker-metadata-action"]
dockerfile = "Dockerfile.app"
context = "./app"
}
target "api" {
inherits = ["docker-metadata-action"]
dockerfile = "Dockerfile.api"
context = "./api"
}
Executing the Build
Build all targets from the default group:
docker buildx bake
Build a specific target or group:
docker buildx bake app
Pass variables:
TAG=v1.0.0 docker buildx bake
Practical Examples
Example 1: Simple Multi-Component Application
Suppose we have an application consisting of a web frontend, API, and database service. Here's what a docker-bake.hcl
file might look like:
variable "TAG" {
default = "latest"
}
group "default" {
targets = ["frontend", "api", "db"]
}
group "services" {
targets = ["api", "db"]
}
target "base" {
context = "."
args = {
BASE_IMAGE = "node:16-alpine"
}
}
target "frontend" {
inherits = ["base"]
dockerfile = "frontend/Dockerfile"
tags = ["myapp/frontend:${TAG}"]
args = {
API_URL = "http://api:3000"
}
}
target "api" {
inherits = ["base"]
dockerfile = "api/Dockerfile"
tags = ["myapp/api:${TAG}"]
args = {
DB_HOST = "db"
DB_PORT = "5432"
}
}
target "db" {
context = "./db"
dockerfile = "Dockerfile"
tags = ["myapp/db:${TAG}"]
}
Example 2: Multi-Platform Building
One of the powerful aspects of Docker Bake is the ease of setting up multi-platform building:
variable "TAG" {
default = "latest"
}
group "default" {
targets = ["app-all"]
}
target "app" {
dockerfile = "Dockerfile"
tags = ["myapp/app:${TAG}"]
}
target "app-linux-amd64" {
inherits = ["app"]
platforms = ["linux/amd64"]
}
target "app-linux-arm64" {
inherits = ["app"]
platforms = ["linux/arm64"]
}
target "app-all" {
inherits = ["app"]
platforms = ["linux/amd64", "linux/arm64"]
}
Example 3: Different Development Environments
Docker Bake makes it easy to manage builds for different environments (e.g., development, testing, and production). For this, you can use variables that are overridden via the command line:
variable "ENV" {
default = "dev"
}
group "default" {
targets = ["app-${ENV}"]
}
target "app-base" {
dockerfile = "Dockerfile"
args = {
BASE_IMAGE = "node:16-alpine"
}
}
target "app-dev" {
inherits = ["app-base"]
tags = ["myapp/app:dev"]
args = {
NODE_ENV = "development"
DEBUG = "true"
}
}
target "app-stage" {
inherits = ["app-base"]
tags = ["myapp/app:stage"]
args = {
NODE_ENV = "production"
API_URL = "https://api.stage.example.com"
}
}
target "app-prod" {
inherits = ["app-base"]
tags = ["myapp/app:prod", "myapp/app:latest"]
args = {
NODE_ENV = "production"
API_URL = "https://api.example.com"
}
}
To build an image for a specific environment, use the command:
ENV=prod docker buildx bake
Advanced Docker Bake Features
Matrix Builds
Docker Bake allows you to define matrices for creating multiple build variants based on parameter combinations:
variable "REGISTRY" {
default = "docker.io/myorg"
}
target "matrix" {
name = "app-${platform}-${version}"
matrix = {
platform = ["linux/amd64", "linux/arm64"]
version = ["1.0", "2.0"]
}
dockerfile = "Dockerfile"
tags = ["${REGISTRY}/app:${version}-${platform}"]
platforms = ["${platform}"]
args = {
VERSION = "${version}"
}
}
This code will create four image variants for each combination of platform and version. You can build them all with a single command.
Using External Files and Functions
Docker Bake allows you to use external files and functions for more flexible configuration:
// Import variables from a JSON file
variable "settings" {
default = {}
}
function "tag" {
params = [name, tag]
result = ["${name}:${tag}"]
}
target "app" {
dockerfile = "Dockerfile"
tags = tag("myapp/app", "v1.0.0")
args = {
CONFIG = "${settings.app_config}"
}
}
Then you can pass a settings file:
docker buildx bake --file settings.json
Integration With Docker Compose
Docker Bake can be integrated with Docker Compose, which is especially convenient for existing projects:
# docker-compose.yml
services:
app:
build:
context: ./app
dockerfile: Dockerfile
args:
VERSION: "1.0"
image: myapp/app:latest
api:
build:
context: ./api
dockerfile: Dockerfile
image: myapp/api:latest
# docker-bake.hcl
target "default" {
context = "."
dockerfile-inline = <<EOT
FROM docker/compose:1.29.2
WORKDIR /app
COPY docker-compose.yml .
RUN docker-compose build
EOT
}
Conditional Logic
For more complex scenarios, you can use conditional logic:
variable "DEBUG" {
default = "false"
}
target "app" {
dockerfile = "Dockerfile"
tags = ["myapp/app:latest"]
args = {
DEBUG = "${DEBUG}"
EXTRA_PACKAGES = DEBUG == "true" ? "vim curl htop" : ""
}
}
Using Docker Bake in CI/CD
Docker Bake is perfect for use in CI/CD pipelines. Here's an example of integration with GitHub Actions, using secrets for secure authentication in Docker Hub:
# .github/workflows/build.yml
name: Build and Publish
on:
push:
branches: [main]
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker Metadata
id: meta
uses: docker/metadata-action@v4
with:
images: myapp/app
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
- name: Build and push
uses: docker/bake-action@v2
with:
files: |
./docker-bake.hcl
targets: app
push: true
set: |
*.tags=${{ steps.meta.outputs.tags }}
Debugging and Monitoring Builds
Docker Bake provides several useful options for debugging the build process:
View configuration without building:
docker buildx bake --print
Detailed logs:
docker buildx bake --progress=plain
Export to JSON for analysis:
docker buildx bake --print | jq
Comparison With Other Tools
Docker Bake vs. Docker Compose
Feature | Docker Bake | Docker Compose |
---|---|---|
Main purpose | Building images | Container management |
Parallel building | Yes, automatically | Limited |
Matrix builds | Yes | No |
Inheritance | Yes, powerful system | Limited (extends) |
Multi-platform | Yes, integrated | No |
Configuration format | HCL, JSON | YAML |
Docker Bake vs. Build Scripts
Aspect | Docker Bake | Bash/scripts |
---|---|---|
Declarativeness | High | Low |
Maintenance complexity | Low | High |
Reusability | Simple | Complex |
Parallelism | Automatic | Manual |
CI/CD integration | Simple | Requires effort |
Best Practices
Organize targets into logical groups:
group "all" {
targets = ["app", "api", "worker"]
}
group "backend" {
targets = ["api", "worker"]
}
Use inheritance for common settings:
target "common" {
context = "."
args = {
BASE_IMAGE = "node:16-alpine"
}
}
target "app" {
inherits = ["common"]
dockerfile = "app/Dockerfile"
}
Organize complex configurations into multiple files:
docker buildx bake \
-f ./common.hcl \
-f ./development.hcl \
app-dev
Use variables for flexibility:
variable "REGISTRY" {
default = "docker.io/myorg"
}
target "app" {
tags = ["${REGISTRY}/app:latest"]
}
Apply matrices for complex build scenarios:
target "matrix" {
matrix = {
env = ["dev", "prod"]
platform = ["linux/amd64", "linux/arm64"]
}
name = "app-${env}-${platform}"
tags = ["myapp/app:${env}-${platform}"]
}
Common Problems and Solutions
Problem 1: Cache is Not Used Efficiently
Solution
Properly structure your Dockerfile, placing layers that change less frequently at the beginning of the file:
FROM node:16-alpine
# First copy only dependency files
COPY package.json package-lock.json ./
RUN npm install
# Then copy the source code
COPY . .
Problem 2: Environment variable conflicts
Solution
Use explicit values in Docker Bake:
target "app" {
args = {
NODE_ENV = "production"
}
}
Problem 3: Difficult to debug builds
Solution
Use detailed logs and inspection:
docker buildx bake --progress=plain --print app
Conclusion
Docker Bake provides a powerful, flexible, and declarative approach to organizing Docker image building. It solves many problems that teams face when using traditional build approaches, especially in complex multi-component projects.
The main advantages of Docker Bake:
- Declarative approach
- Efficient cache usage
- Parallel and multi-platform building
- Powerful variable and inheritance system
- Excellent integration with CI/CD pipelines
Implementing Docker Bake in your workflow can significantly simplify and speed up image-building processes, especially for teams working with microservice architecture or complex multi-component applications.
Useful Resources
Opinions expressed by DZone contributors are their own.
Comments