Stop Writing Ugly Docker Build Commands Today — Do This Instead

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 repos with different platforms, and then you will be using way more flags. That means way more googling or prompting AI bots.
Are you looking to advance your DevOps career?
Join my 16-week Advanced, real-world, project-based DevOps Bootcamp is for you.
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.
A Must Read:
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-south1.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.

Now just run one command
docker buildx bake

Check 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 adocker 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 rundocker 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 setscontext
and multi-platform builds.backend
andfrontend
inherit fromcommon
, 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)
// 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 HTTPS URL

Bake file reference
You can write Bake files in HCL, YAML (Docker Compose files), or JSON.
I used docker-bake.hcl
as 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