Docker Bake: A Practical Guide to Declarative, Parallel Docker Builds
Replace scattered docker build commands with a single bake.hcl config. Run multi-service builds in parallel with shared cache and zero copy-paste drift.
You've got a docker build command for the API, another for the worker, another for the frontend, and a fourth for that internal admin tool nobody remembers deploying last week. Each has its own build args, its own platform flags, its own tag scheme. Coordinating all of them across your local dev setup, your CI pipeline, and every engineer's machine creates friction: every time someone adds a service, they manually add another build step, and subtle differences creep in between environments.
Docker Bake fixes this by letting you describe all of those builds — targets, arguments, platforms, output types — in a single configuration file, then run every build (or a chosen subset) with one command, in parallel, using shared build context and cache.
What Is Docker Bake?
Docker Bake is a build orchestration feature built on top of BuildKit (the same engine behind docker buildx build). Instead of invoking docker build once per image, you define targets in a bakefile — a declarative description of what to build, from where, with which arguments, tags, and platforms — and then run docker buildx bake to execute some or all of those targets in a single invocation.
Think of it as the build-time equivalent of docker-compose.yml. Compose describes how services run together at runtime; a bakefile describes how images get built together at build time. In fact, Bake can read target definitions directly from a compose.yaml file's build: sections, which makes it a natural next step if you already have a multi-service Compose setup and just never had a clean way to build all those services at once.
A bakefile can be written in three formats:
- HCL (
docker-bake.hcl) — the native, purpose-built format, with variables, functions, and composability - YAML — for teams that want Bake config to look and feel like their Compose files
- JSON — useful for generating bakefiles programmatically from scripts or other tooling
All three describe the same concepts: targets (individual image builds) and groups (named collections of targets you want to run together).
Why Complex Docker Build Setups Break Down
If you've only ever built one image, docker build -t myapp . is all you need. The pain shows up once your project has more than a couple of images that need to be built together, consistently, and often.
Here's what that looks like in practice:
Every one of these commands is imperative, sequential (unless you background them and manage wait yourself), and duplicated across your local dev scripts, your Makefile, and your CI YAML — usually with subtle drift between the three. Common failure modes:
- Argument drift. The
NODE_ENVbuild arg gets updated in CI but not in the local build script, so "works on my machine" becomes "works in my build." - No shared context or cache logic. Each
docker buildinvocation re-sends build context and re-resolves cache independently, even when several services share a monorepo root. - Platform flags scattered everywhere. Multi-platform builds (
--platform linux/amd64,linux/arm64) get bolted onto some commands and forgotten on others. - Sequential by default. Builds run one after another unless you explicitly parallelize with background jobs — which most people don't bother doing until CI gets slow enough to complain about.
- No single source of truth. The "how do we build this thing" knowledge lives in a Makefile, a shell script, and three Confluence pages, none of which agree.
None of these are fatal individually. Together, across a growing microservices repo, they add up to a build process nobody fully trusts.
The Declarative Alternative: One File, One Command
A bakefile collapses all of that into a single declarative definition. You describe what each target needs — its context, Dockerfile, build args, tags, platforms — once, and Bake figures out how to execute them, including running independent targets concurrently.
The mental shift is the same one you made going from standalone docker run commands to docker-compose up: you stop thinking in individual imperative steps and start thinking in terms of a build graph.
A Working Bakefile (HCL)
Here's a bakefile for a small monorepo with an API, a worker, and a frontend:
Run every target in the default group with one command:
Run a single target:
Run a subset by naming multiple targets or a group:
Override a variable at the command line without touching the file:
That's the entire workflow: one file describing every build, one command to run any combination of them.
The Same Thing in YAML
If your team already thinks in Compose syntax, the YAML form will feel immediately familiar:
The Same Thing in JSON
JSON is the least pleasant to hand-write but the easiest to generate from a script — useful if you have a service registry or monorepo tool that already knows the full list of buildable services and can emit this structure automatically.
Running Multiple Builds in Parallel
The single most practical win of Bake is that independent targets build concurrently by default when you run docker buildx bake against a group. In the example above, api, worker, and frontend don't depend on each other, so a single docker buildx bake invocation builds all three at once rather than marching through them one at a time.
You can organize groups around how your team actually works, not just "everything":
Then:
This is the piece that directly answers the "how do I orchestrate multiple/parallel builds" question: you don't write parallelization logic yourself. You describe the targets, group them how you like, and Bake's build graph handles concurrency for independent targets.
Multi-Platform Build Matrices
Multi-architecture images (linux/amd64 + linux/arm64, for instance) are one of the more tedious things to manage by hand because every target that needs it requires its own --platform flag, and it's easy to forget one when a new service is added.
In a bakefile, platforms is just another attribute per target:
You can also define a reusable base target with inherits so every image in the matrix picks up the same platform list without repeating it:
This composability — building shared attributes once and inheriting them across targets — is one of the more useful HCL-specific features, and it's the difference between a bakefile that scales cleanly to a dozen services and one that becomes its own copy-paste problem.
Wiring Bake Into CI/CD
Bake's CI value is straightforward: replace a sequence of docker build steps in your pipeline with one docker buildx bake call, driven by a checked-in bakefile so your CI build behavior matches what engineers run locally.
Docker publishes an official GitHub Action for this, commonly used alongside the docker/setup-buildx-action step to configure the BuildKit builder first. A minimal GitHub Actions job looks like this:
The set input lets you override bakefile values from the pipeline — tagging every image with the commit SHA, for instance — without maintaining a separate CI-only bakefile. Your local docker buildx bake and your pipeline's docker/bake-action step read the exact same file, which is the whole point: one definition of "how we build this," used everywhere.
Where Bake Genuinely Pays Off — and Where It's Overkill
Bake earns its keep in a few specific situations:
- Monorepos with several independently buildable services. If you're maintaining build args, tags, and platform lists for more than a handful of images, a single bakefile beats a sprawling Makefile or shell script every time.
- Microservices architectures with shared build conventions. Composable attributes (via
inherits) let you define a base target once — common build args, common platforms, common labels — and have every service target reuse it, so a policy change (like adding anarm64target) happens in one place. - CI/CD pipelines that need parallel, consistent builds. Once a bakefile exists, CI just calls it — no drift between what a developer runs locally and what the pipeline runs.
- Multi-platform image matrices. Bake removes the need to hand-manage
--platformflags across manydocker buildinvocations.
Bake is comparative overkill in a few common cases:
- A single-image project. If you have one Dockerfile and one build target, a bakefile adds a layer of indirection for no real gain over
docker build. - Highly dynamic, one-off build scripts. If your build parameters change per invocation in ways that don't map cleanly to reusable targets (e.g., a build tool generating ad hoc images for testing), a bakefile can feel like unnecessary ceremony compared to a shell script.
- Teams unfamiliar with BuildKit/Buildx concepts who need to ship immediately. Bake assumes comfort with Buildx; if your team hasn't adopted Buildx yet, that's the more fundamental first step.
Getting Started
If you want to try this on an existing project: pick your two or three most-built images, write a docker-bake.hcl with one target per image and a default group tying them together, then replace your existing build script's docker build calls with a single docker buildx bake call. Run it once locally to confirm the tags and platforms match what you expect, then move the same file into your CI pipeline via the official Bake GitHub Action.
The value compounds as you add services — each new target is a few lines in a file everyone already understands, not a new block of imperative commands somebody has to remember to keep in sync.
FAQ
What is Docker Bake, in one sentence? Docker Bake is a BuildKit-based orchestration tool that lets you define multiple Docker image builds — their contexts, arguments, tags, and platforms — declaratively in a single file, then execute any combination of them with one command.
Does Bake replace docker build?
No. Bake runs on the same BuildKit engine as docker buildx build; it's a layer on top that lets you describe and coordinate multiple builds instead of invoking docker build repeatedly by hand.
Can I use my existing Compose file with Bake?
Bake can read target definitions from a Compose file's build: sections, which is a convenient path if you already have multi-service Compose configuration and want to build those services together without duplicating the definitions.
HCL, YAML, or JSON — which should I pick? HCL is the native format and supports the most composability features (variables, functions, inheritance). YAML is a good fit if your team already thinks in Compose syntax. JSON is best when you're generating the bakefile programmatically rather than hand-writing it.
Do targets run in parallel automatically?
Yes — when you run docker buildx bake against a group of independent targets, Bake builds them concurrently rather than sequentially, without you writing any parallelization logic yourself.
Is Bake worth adopting for a single Dockerfile project?
Generally not. The declarative structure pays off once you're coordinating multiple images, shared build arguments, or multi-platform matrices — for one image, plain docker build is simpler.
Damian Hodgkiss
Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.