Docker Bake in Practice โ€” Part 1: From Bash Scripts to Declarative Builds
DEV Community

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"]
Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.