Why Docker Bake is the Smarter Way to Build and Manage Docker Images
Do you enjoy remembering the docker build commands with all the different flags, and arguments?
Well, I never did. I had to google the flag and arguments whenever I needed to build images.
It gets worse when I have to build multiple docker images for mono repositories with different platforms.
Then, you will be using way more flags. That means way more googling or prompting AI bots.
What if I tell you a better way that allows you to write docker build as code and build everything with just one short command?
Well, you are in luck as good people at Docker finally released their long-awaited, build orchestration tool, Docker Bake in General Availability.
It is available with the Docker Desktop version 4.38.
Introducing Docker bake
Docker Bake allows you to define build stages and deployment environments in a declarative file, making complex builds easier to manage. It also leverages BuildKit's parallelization and optimization features to speed up build times.
Let me show you why I love Docker Bake
Let's take the example of a mono repo with a dummy frontend and backend application code with its Dockerfile.
If I had to build the docker images for both of these applications and push these two, I would have to run these commands run 2 commands
# Frontend build
docker build --build-arg NODE_VERSION=20 -t \
366140438193.dkr.ecr.ap-south-1.amazonaws.com/frontend:latest \
-f frontend/frontend.Dockerfile frontend
# Backend Build
docker build --build-arg GO_VERSION=1.21 -t \
366140438193.dkr.ecr.ap-south-1.amazonaws.com/backend:latest \
-f backend/backend.Dockerfile backend
And if you want to push these images to ECR repos, you will be running
docker push 366140438193.dkr.ecr.ap-south-1.amazonaws.com/frontend:latest
docker push 366140438193.dkr.ecr.ap-south-1.amazonaws.com/backend:latest
I used multiple flags to build the app, and 4 commands to push the image and you don't have these commands version-controlled as these are just commands.
Now what if I can tell you that you don't need to remember these flags and store these commands just like Terraform infra templates?
Yes, you can do it all with just one command - Docker buildx bake
Let me show you how Docker Bake makes all this a piece of cake.
Create a file docker-bake.hcl into the root path and paste the below into that file.
# docker-bake.hcl
group "default" {
targets = ["frontend", "backend"]
}
target "frontend" {
context = "./frontend"
dockerfile = "frontend.Dockerfile"
args = {
NODE_VERSION = "20"
}
tags = ["366140438193.dkr.ecr.ap-south-1.amazonaws.com/frontend:latest"]
}
target "backend" {
context = "./backend"
dockerfile = "backend.Dockerfile"
args = {
GO_VERSION = "1.21"
}
tags = ["366140438193.dkr.ecr.ap-south-1.amazonaws.com/backend:latest"]
}
Your project will look like this.
docker buildx bake
Checking if the new Docker images exist by running the docker images command.
You can see that the docker build bake command built both images using the HCL config file.
You can push the docker-bake.hcl to your GitHub repo, no one ever needs to use remember docker build commands or its flags.
You can also push the images to remote ECR repositories with --push argument
docker buildx bake --push
Digging deep into Docker bake
If you have a docker command like below:
docker build \
-f Dockerfile \
-t myapp:latest \
--build-arg foo=bar \
--no-cache \
--platform linux/amd64,linux/arm64 \
.
It's equivalent Docker Bake file will be like this:
# docker-bake.hcl
target "myapp" {
context = "."
dockerfile = "Dockerfile"
tags = ["myapp:latest"]
args = {
foo = "bar"
}
no-cache = true
platforms = ["linux/amd64", "linux/arm64"]
}
A target in a Bake file represents a build invocation.
It holds all the information you would normally pass on to a docker build command using flags.
To build a target with Bake, pass the name of the target to the bake command -
docker buildx bake webapp
If you don't define any target, the bake command will build the default target.
target "default" {
dockerfile = "webapp.Dockerfile"
tags = ["docker.io/username/webapp:latest"]
context = "https://github.com/username/webapp"
}
You can run
docker buildx bake
and it will build the image
Let's consider this bake config
group "all" {
targets = ["webapp", "api", "tests"]
}
target "webapp" {
dockerfile = "webapp.Dockerfile"
tags = ["docker.io/username/webapp:latest"]
context = "https://github.com/username/webapp"
}
target "api" {
dockerfile = "api.Dockerfile"
tags = ["docker.io/username/api:latest"]
context = "https://github.com/username/api"
}
target "tests" {
dockerfile = "tests.Dockerfile"
contexts = {
webapp = "target:webapp",
api = "target:api",
}
output = ["type=local,dest=build/tests"]
context = "."
}
To build multiple targets at once, run
docker buildx bake webapp api tests
You can group targets using the group block
docker buildx bake all
If you noticed in the example I talked about at the start, I used a group block with a default target and I used docker build bake to build both images.
group "default" {
targets = ["frontend", "backend"]
}
Inheritance in Bake
Inheritance allows you to define common configurations and reuse them across multiple targets. Consider the below config
target "common" {
context = "."
platforms = ["linux/amd64", "linux/arm64"]
}
target "backend" {
inherits = ["common"]
dockerfile = "backend.Dockerfile"
args = {
GO_VERSION = "1.21"
}
}
target "frontend" {
inherits = ["common"]
dockerfile = "frontend.Dockerfile"
args = {
NODE_VERSION = "20"
}
}
Common target sets context and multi-platform builds.
backend and frontend inherit from common, ensuring they both build for multiple platforms.
Overriding Values in Inheritance
When a target inherits another target, it can override any of the inherited attributes.
For example, the following target overrides the args attribute from the inherited target:
target "base" {
context = "."
dockerfile = "Dockerfile"
args = {
APP_ENV = "development"
}
}
target "production" {
inherits = ["base"]
args = {
APP_ENV = "production"
}
}
Using Variables in Bake - Just like Terraform
You can define and use variables in a Bake file to set attribute values, interpolate them into other values, and perform arithmetic operations.
Consider the below config
group "default" {
targets = [ "frontend" ]
}
variable "NODE_VERSION" {
default = "20"
}
variable "tag" {
default = "latest"
}
target "frontend" {
context = "."
dockerfile = "frontend.Dockerfile"
args = {
NODE_VERSION = NODE_VERSION
}
tags = ["myapp-frontend:${tag}"]
}
Printing the Bake file with the --print flag shows the interpolated value in the resolved build configuration.
docker buildx bake --print
You can do a lot of things like Validating multiple conditions, Escaping variable interpolation
Airthematic expression and Ternary conditional with Docker bake
variable "FOO" {
default = 3
}
variable "IS_FOO" {
default = true
}
target "app" {
args = {
v1 = FOO > 5 ? "higher" : "lower"
v2 = IS_FOO ? "yes" : "no"
}
}
Using built-in and user-defined functions with Docker bake
We can use a user-defined function to generate a tag with a timestamp(with a built-in function)
// User-defined function to generate a tag with a timestamp
function "generate_tag" {
params = [version]
result = format("%s-%s", version, replace(timestamp(), "[-:TZ]", "")) // Correct concatenation
}
// Define a variable for version
variable "APP_VERSION" {
default = "1.0.0"
}
// Define a target using the custom function and built-in function
target "myapp" {
context = "."
dockerfile = "Dockerfile"
tags = ["myapp:${generate_tag(APP_VERSION)}"]
args = {
BUILD_DATE = timestamp()
}
}
Remote Bake file definition
You can build Bake files directly from a remote Git repository or an HTTPS URL
Bake file reference
You can write Bake files in HCL, YAML (Docker Compose files), or JSON.
I used docker-bake.hcl as a file name but you can use any filename you want and pass that file as an argument to the bake command.
docker buildx bake --file ../docker/bake.hcl
By default, Bake uses the following lookup order to find the configuration file.
Docker bake is a powerful tool, there are so many things you can do with it. It offers great flexibility with options like using a matrix, docker-compose like config, overriding configuration, cache, and so many things.
Go check it out and try to use it in your workflow. I am sure you will love it.
Thanks for reading, do share your thoughts in the comments. Would love to hear any feedback/suggestions
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.