Docker Bake in Practice - Part 1: From Bash Scripts to Declarative Builds
Docker Bake in Practice - Part 1: From Bash Scripts to Declarative Builds
Why this article exists
For the past two years I've been giving a talk about Docker Bake at conferences across France and Morocco - DevLille, DevFest Toulouse, DevFest Lyon, Devoxx Morocco, and Cloud Native Days France. Every time I deliver it, the same thing happens: people come up afterwards and say "I had no idea Bake could do that" or "I've been writing 200-line bash scripts to do exactly this." If you want the video version in French, you can watch the Cloud Native Days France recording. An English video walking through these articles is coming soon on my YouTube channel - subscribe if you'd like to be notified. If you prefer to read at your own pace, with copy-pasteable snippets, this article is for you. All the code in this article comes from my companion repository - each section links to the relevant file so you can run the examples yourself. tosun-si / docker-bake-playground A curated collection of concrete, practical, and reusable examples for mastering Docker Bake.
Build the images with Docker Bake locally
docker buildx bake -f vars.hcl -f docker-bake-app-and-infra.hcl
Build and publish the images with Docker Bake locally
docker buildx bake -f vars.hcl -f docker-bake-app-and-infra.hcl --push
Printing the Bake file with the --print flag shows the interpolated value in the resolved build configuration.
docker buildx bake -f vars.hcl -f docker-bake-app-and-infra.hcl --print
Build the images for linter and tests
docker buildx bake -f vars.hcl -f docker-bake-lint-and-test.hcl
Run linter and tests with Compose
docker compose -f compose_lint_and_test.yaml up
Build the multiples Bake files locally
docker buildx bake -f vars.hcl -f docker-bake-app-and-infra.hcl -f docker-bake-lint-and-test.hcl default
Validate
docker buildx bake -f vars.hcl -f docker-bake-app-and-infra.hcl default validate
Print multiple Bake files
docker buildx bake -f vars.hcl -f docker-bake-app-and-infra.hcl -f docker-bake-lint-and-test.hcl default validate --print
Build Bake file with inheritance
docker buildx bake -f vars.hcl -f docker-bake-inheritance.hcl
Build Bake file with matrix variants
docker buildx bake -f vars.hcl -f docker-bake-app-and-infra.hcl
The foundation: BuildKit, Buildx, and Bake
Layering
+----------------------------------------------------+
| Bake (declarative orchestration) |
| docker buildx bake -f file.hcl |
+----------------------------------------------------+
| Buildx (CLI plugin / frontend) |
| docker buildx build ... |
+----------------------------------------------------+
| BuildKit (build engine / backend) |
| parallel stages, cache mounts, multi-platform |
+----------------------------------------------------+
BuildKit
BuildKit is the modern Docker build engine. It replaced the legacy builder a few years ago and is now the default in Docker Engine 23+. BuildKit is the piece doing the actual work: it parses your Dockerfile, builds stages in parallel where it can, manages the cache, handles multi-platform builds, mount caches, secrets, SSH forwarding, and so on.
Buildx
Buildx is a Docker CLI plugin that exposes BuildKit's features through a friendlier interface. When you type docker buildx build ..., you're using Buildx as the frontend and BuildKit as the engine. Buildx also manages builders - named BuildKit instances you can swap between (local, remote, container-driven).
Bake
Bake is a subcommand of Buildx: docker buildx bake. It takes one or more declarative files (HCL, JSON, or a Compose file) and orchestrates many builds at once. Think of it as docker-compose but for building instead of running. You define your images once, in one place, and Bake builds them all - in parallel, with shared variables, inheritance, matrices, and groups.
The traditional approach: bash scripts
Pain points
- Sequential builds
- Duplication of code
- Scales linearly
- Not portable across operating systems
Bash script example
#!/usr/bin/env bash
set -euo pipefail
docker build \
--platform linux/amd64,linux/arm64 \
--file app/Dockerfile \
--tag "${REPO_URL}/app_bake:${IMAGE_TAG_VERSION_APP}" \
--provenance=true \
--sbom=true \
--push \
.
docker build \
--platform linux/amd64,linux/arm64 \
--file infra/Dockerfile \
--tag "${REPO_URL}/infra_bake:${IMAGE_TAG_VERSION_INFRA}" \
--provenance=true \
--sbom=true \
--push \
.
Enter Bake: same images, declaratively
Bake file example
group "default" {
targets = ["app", "infra"]
}
target "app" {
context = "."
dockerfile = "images/app/Dockerfile"
platforms = ["linux/amd64", "linux/arm64"]
tags = ["${REPO_URL}/app_bake:${IMAGE_TAG_VERSION_APP}"]
attest = ["type=provenance,mode=max", "type=sbom"]
}
target "infra" {
context = "."
dockerfile = "images/infra/Dockerfile"
platforms = ["linux/amd64", "linux/arm64"]
tags = ["${REPO_URL}/infra_bake:${IMAGE_TAG_VERSION_INFRA}"]
attest = ["type=provenance,mode=max", "type=sbom"]
}
Building with Bake
docker buildx bake -f vars.hcl -f docker-bake-app-and-infra.hcl
Building and pushing with Bake
docker buildx bake -f vars.hcl -f docker-bake-app-and-infra.hcl --push
Variables and validators
Variables
variable "PROJECT_ID" {
validation {
condition = PROJECT_ID != ""
error_message = "The variable 'PROJECT_ID' must not be empty."
}
}
variable "LOCATION" {
default = "europe-west1"
}
variable "REPO_NAME" {
validation {
condition = REPO_NAME != ""
error_message = "The variable 'REPO_NAME' must not be empty."
}
}
variable "IMAGE_TAG_VERSION_APP" {
validation {
condition = IMAGE_TAG_VERSION_APP != ""
error_message = "The variable 'IMAGE_TAG_VERSION_APP' must not be empty."
}
}
variable "REPO_URL" {
default = "${LOCATION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}"
}
Validators
Bake supports validation blocks on variables. If PROJECT_ID is missing or empty at build time, the build fails immediately with a clear message - before a single layer gets built.
Inheritance: the _common pattern
Inheritance example
group "default" {
targets = ["app", "infra"]
}
target "_common" {
context = "."
platforms = ["linux/amd64", "linux/arm64"]
attest = ["type=provenance,mode=max", "type=sbom"]
}
target "app" {
inherits = ["_common"]
dockerfile = "images/app/Dockerfile"
tags = ["${REPO_URL}/app_bake:${IMAGE_TAG_VERSION_APP}"]
}
target "infra" {
inherits = ["_common"]
dockerfile = "images/infra/Dockerfile"
tags = ["${REPO_URL}/infra_bake:${IMAGE_TAG_VERSION_INFRA}"]
}
Python lint and test images with uv
Dockerfile example
FROM ghcr.io/astral-sh/uv:python3.11-alpine AS builder
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
WORKDIR=/usr/local/src/app
WORKDIR $WORKDIR
COPY pyproject.toml uv.lock ./RUN --mount=type=cache,target=/root/.cache/uv uv sync --lockedCOPY python_app $WORKDIR/python_appFROM python:3.11-alpineENV WORKDIR=/usr/local/src/appWORKDIR $WORKDIRCOPY --from=builder $WORKDIR $WORKDIRENV PATH="$WORKDIR/.venv/bin:$PATH"ENTRYPOINT ["ruff"]CMD ["check", "python_app", "--exclude"]
Comments
No comments yet. Start the discussion.