Dans l'écosystème DevOps moderne, la construction d'images Docker est devenue une opération quotidienne. Mais nos scripts de build ressemblent souvent à un patchwork de commandes shell et de docker build séquentiels qui ralentissent nos pipelines. Docker Bake apporte une approche plus élégante, standardisée et performante pour orchestrer nos builds Docker.
Docker Bake : qu'est-ce que c'est ?
Aujourd'hui, le build d'images Docker repose sur BuildKit, le moteur de build moderne qui gère la parallélisation, le cache avancé et les secrets mounts, accessible via Buildx, l'interface CLI qui permet notamment les builds multi-plateformes.
Docker Bake est une surcouche déclarative de Buildx qui permet de définir et d'orchestrer nos builds dans des fichiers de configuration standardisés (HCL ou YAML). Plutôt que d'enchaîner des commandes docker build dans des scripts shell, nous déclarons nos targets, leurs dépendances et leurs paramètres, et Bake se charge de paralléliser intelligemment les builds tout en optimisant le cache et la réutilisation des layers.
Les limites de l'approche traditionnelle
Scripts shell fragiles et OS-dépendants
Dès qu'on veut faire les choses "proprement" avec validation des variables et factorisation, la complexité explose :
#!/bin/bash
set -e
# Validation des variables
if [ -z "$REGISTRY_URL" ]; then
echo "Error: REGISTRY_URL is not set"
exit 1
fi
# Vérifier que l'URL du registry est valide (regex différent selon l'OS)
if [[ ! "$REGISTRY_URL" =~ ^[a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Error: Invalid REGISTRY_URL format"
exit 1
fi
if [ -z "$TAG" ]; then
TAG="latest"
fi
# Factorisation avec une boucle
SERVICES=("app" "worker" "api")
for service in "${SERVICES[@]}"; do
echo "Building $service..."
docker build \
-t "$REGISTRY_URL/$service:$TAG" \
-f "Dockerfile.$service" \
--build-arg REGISTRY_URL="$REGISTRY_URL" \
.
if [ $? -ne 0 ]; then
echo "Failed to build $service"
exit 1
fi
doneProblèmes de ce script :
- Beaucoup de lignes de code pour seulement 3 builds
- Ne fonctionne pas sur tous les shells / OS
- Une maintenance plus difficile si de nouvelles variables s’ajoutent.
- Mais surtout, les builds restent séquentielles ! On a donc un temps d’exécution qui peut être optimisé.
Docker Bake : les concepts fondamentaux
Séparation configuration / définition
Docker Bake encourage la séparation entre la configuration des variables (vars.hcl) et la définition des builds (docker-bake.hcl), un peu comme pour du code Terraform.
vars.hcl - La configuration des valeurs avec validation :
variable "TAG" {
default = "latest"
}
variable "PLATFORMS" {
default = ["linux/amd64", "linux/arm64"]
type = list(string)
}
variable "REGISTRY" {
default = "myregistry.io/myapp"
type = string
}
variable "PYTHON_VERSION" {
default = "3.12"
type = string
}docker-bake.hcl - La définition des builds :
target "python-app" {
name = "python-app-${replace(python_version, ".", "-")}-${replace(platform, "/", "-")}"
context = "."
dockerfile = "Dockerfile"
platforms = [platform]
args = {
PYTHON_VERSION = python_version
}
tags = ["${REGISTRY}/python-app:${python_version}"]
cache-from = ["type=registry,ref=${REGISTRY}/python-app:cache-py${python_version}"]
cache-to = ["type=registry,ref=${REGISTRY}/python-app:cache-py${python_version},mode=max"]
matrix = {
python_version = PYTHON_VERSIONS
platform = PLATFORMS
}
}
group "default" {
targets = ["python-app"]
}Exécution :
# Voir la configuration en dry-run (très utile pour le debug)
docker buildx bake -f vars.hcl -f docker-bake.hcl --print
# Builder le groupe "default" (pas besoin de le préciser s'il est seul)
docker buildx bake -f vars.hcl -f docker-bake.hcl
# Builder une target spécifique
docker buildx bake -f vars.hcl -f docker-bake.hcl webapp
# Override une variable
TAG=2.0.0 docker buildx bake -f vars.hcl -f docker-bake.hclPourquoi HCL ?
Docker recommande HCL car il offre plus de fonctionnalités : expressions arithmétiques, fonctions personnalisées, opérateurs ternaires et syntaxe plus lisible pour les configurations complexes.
Les fonctionnalités différenciantes
Parallélisation intelligente : Bake analyse automatiquement les dépendances entre targets et parallélise les builds indépendants. Contrairement à des jobs CI parallélisés, les builds partagent le même contexte BuildKit, permettant une construction optimale des layers. Si certains layers sont identiques (ont le même hash), ils ne seront pas build plusieurs fois, mais plutôt réutilisés, ce qui fait gagner du temps de build.
Portabilité garantie : Un fichier docker-bake.hcl produit le même résultat sur Linux, macOS et Windows.
Validation des variables : Si vous passez une valeur du mauvais type, Bake échoue immédiatement avant même de démarrer le build.
Cache avancé : Intégration native avec les mécanismes de cache de BuildKit (registry, local, etc.)
Build matriciel : la killer feature
Le build matriciel génère automatiquement plusieurs variantes d'une image en croisant différentes variables.
vars.hcl :
variable "PYTHON_VERSIONS" {
default = ["3.10", "3.11", "3.12"]
type = list(string)
}
variable "PLATFORMS" {
default = ["linux/amd64", "linux/arm64"]
type = list(string)
}
variable "REGISTRY" {
default = "docker-registry.ippon.fr/amorin/helloworldspringboot"
type = string
}docker-bake.hcl :
target "python-app" {
name = "python-app-${replace(python_version, ".", "-")}-${replace(platform, "/", "-")}"
context = "."
dockerfile = "Dockerfile"
platforms = [platform]
args = {
PYTHON_VERSION = python_version
}
tags = ["${REGISTRY}/python-app:${python_version}"]
cache-from = ["type=registry,ref=${REGISTRY}/python-app:cache-py${python_version}"]
cache-to = ["type=registry,ref=${REGISTRY}/python-app:cache-py${python_version},mode=max"]
matrix = {
python_version = PYTHON_VERSIONS
platform = PLATFORMS
}
}Cette configuration génère 6 builds automatiquement (3 versions × 2 plateformes), tous exécutés en parallèle avec un cache partagé.
Comparaison avec les matrices CI/CD
Les matrices CI/CD créent des jobs parallèles complètement isolés. Chaque job démarre son propre runner, télécharge le code, initialise BuildKit et n'a aucun partage de cache avec les autres jobs.
Les matrices Docker Bake s'exécutent dans un seul job CI/CD avec un BuildKit partagé qui mutualise le cache layers entre tous les variants, déduplique automatiquement les layers communs et parallélise intelligemment les builds. Ceci permettrait par exemple pour un build à 6 variants (3 versions × 2 plateformes) de passer potentiellement de 30 minutes (6 jobs × 5 min) à 8 minutes (démarrage + builds parallèles avec cache partagé), car l'image de base n'est téléchargée qu'une seule fois.
Exemple pratique : optimisation Python avec uv
L'une des astuces partagées lors de la conférence était l'utilisation de uv, le gestionnaire de paquets Python ultra-rapide (10-100x plus rapide que pip). Couplé avec les builds matriciels de Docker Bake (fichiers docker-bake.hcl et vars.hcl précédents), vous pouvez facilement générer des images optimisées pour Python 3.10, 3.11 et 3.12 sur différentes architectures, le tout avec un cache partagé qui accélère drastiquement l'ensemble du processus.
Dockerfile optimisé :
# Stage 1: Build
FROM ghcr.io/astral-sh/uv:python3.12-alpine AS builder
WORKDIR /app
# Copier d'abord les fichiers de dépendances pour optimiser le cache
COPY pyproject.toml uv.lock ./
# Installer les dépendances avec cache mount
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-install-project --no-dev
# Copier le code de l'application
COPY python_app ./python_app
# Installer le projet
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev
# Stage 2: Runtime
FROM python:3.12-alpine
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/python_app /app/python_app
ENV PATH="/app/.venv/bin:$PATH"
CMD ["python", "-m", "python_app"]Le RUN --mount=type=cache,target=/root/.cache/uv est essentiel : il permet à BuildKit de persister le cache de uv entre les builds pour des builds incrémentiels ultra-rapides.
Intégration CI/CD
Quand utiliser Bake vs des jobs CI parallélisés ?
Utilisez Docker Bake quand :
- Vous buildez plusieurs images qui partagent des layers communs
- Vous avez besoin de builds multi-plateformes ou multi-versions
- Vous voulez standardiser votre processus de build entre dev et CI
Utilisez des jobs CI parallèles quand :
- Vos images sont complètement indépendantes sans layers partagés
- Vous avez besoin d'isolation stricte entre les builds
- Chaque build nécessite des ressources très différentes
Vous pouvez également combiner les deux en créant des groupes Bake qui s'exécutent dans différents jobs CI pour optimiser à la fois l'isolation et le partage de cache.
Conseils pratiques
Débogage : Utilisez --print pour voir la configuration résolue et --progress=plain pour un mode verbose.
Variables d'environnement : Toutes les variables peuvent être overridées via l'environnement pour adapter les builds selon le contexte.
Intégration Compose : Bake peut lire directement des fichiers docker-compose.yml et extraire les configurations de build.
Gestion des secrets
Pour gérer les secrets (tokens NPM, credentials Git, etc.), définissez-les dans votre docker-bake.hcl :
target "webapp" {
context = "."
dockerfile = "Dockerfile"
tags = ["${REGISTRY}/webapp:${TAG}"]
secret = [
"id=npm_token,src=${HOME}/.npmrc"
]
}Dans le Dockerfile :
RUN --mount=type=secret,id=npm_token,target=/root/.npmrc \
npm installLe secret n'est jamais persisté dans l'image finale, ce qui est très utile !
Conclusion
Docker Bake nous montre une façon plus élégante de faire de la construction d'images Docker. En combinant déclaration, standardisation et performance, il répond aux besoins des équipes DevOps gérant des environnements complexes. La prochaine fois que vous vous retrouverez à gérer plusieurs images Docker dans votre projet, à jongler avec des builds multi-plateformes ou multi-versions, ou simplement à chercher un moyen de standardiser vos processus de build tout en optimisant vos temps de CI/CD, pensez à Docker Bake !
Ressources : Documentation Docker Bake • BuildKit • uv