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.

Source: Image provided by Author

Now just run one command

docker buildx bake
Source: Image provided by Author

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
Source: Image provided by Author

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 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 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)

// 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

Akhilesh Mishra

Akhilesh Mishra

I am Akhilesh Mishra, a self-taught Devops engineer with 11+ years working on private and public cloud (GCP & AWS)technologies.

I also mentor DevOps aspirants in their journey to devops by providing guided learning and Mentorship.

Topmate: https://topmate.io/akhilesh_mishra/