Patrice Ferlet
Patrice Ferlet
Créateur de ce blog.
Publié le 6 août 2025 Temps de lecture: 15 min

Me suivre sur Mastodon

Exit Kaniko et BuildX, place à Buildah

thumbnail for this post

Kaniko est mort, et tout le monde se rue sur BuildX. Parce que Docker en parle, car ça parait simple. Et au milieu de tout ça, se trouve Buildah. Et il devrait être la réponse par défaut chez les DevOps.

TL;DR

Utilisez Buildah pour construire des images OCI dans vos pipelines CI/CD. Avec les commandes :

buildah build -t mon-image:latest /tmp/monimage
buildah push ...

Ou bien: utilisez l’API pour faire des images sans Containerfile / Dockerfile si cela vous simplifie la vie.

Pour les actions GitHub, ce template vous servira, en adaptant (par exemple pour créer le tag avec github.ref_name et ajouter le “latest”, en ajoutant des labels, etc.) :

name: Build image

# changez en on push, on release, etc. selon vos besoins
on: workflow_dispatch

# pour la suite
env:
  IMAGE_NAME: ${{ github.repository }}
  REGISTRY: ghcr.io

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Install Buildah
        run: |-
          sudo apt-get update
          sudo apt-get install -y buildah
      - name: Buildah build
        run: |-
          buildah build -t $REGISTRY/${IMAGE_NAME} -f ./oci/Containerfile .
          # puis... 
          # buiildah tag $REGISTRY/${IMAGE_NAME} $REGISTRY/${IMAGE_NAME}:latest
          # buildah tag $REGISTRY/${IMAGE_NAME} $REGISTRY/${IMAGE_NAME}:${{ github.ref_name }}
      - name: Buildah Push
        run: |-
          buildah login $REGISTRY -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
          buildah push $REGISTRY/${IMAGE_NAME,
          # et si vous avez taggé, vous pouvez faire
          # buildah push $REGISTRY/${IMAGE_NAME}:latest
          # buildah push $REGISTRY/${IMAGE_NAME}:${{ github.ref_name }}
          # ...

Ou utilisez les actions de Red Hat :

env:
  IMAGE_NAME: ${{ github.repository }}
  REGISTRY: ghcr.io

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Build image
        id: build-image
        uses: redhat-actions/buildah-build@v2
        with:
          image: ${{ env.REGISTRY}}/${{ env.IMAGE_NAME }}
          tags: latest ${{ github.ref_name }}
          containerfiles: |
            ./oci/Containerfile
      - name: Push image
        uses: redhat-actions/push-to-registry@v2
        with:
          image: ${{ steps.build-image.outputs.image }}
          tags: ${{ steps.build-image.outputs.tags }}
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

Maintenant, si vous voulez en savoir plus sur Buildah, continuez la lecture.

L’origine du bazar

Sur votre machine, vous “buildez” vos images Docker tranquille avec un petit docker build, tout roule.

Puis arrive la CI/CD… et là, c’est le drame. Les runners tournent souvent en conteneur, et Docker, lui, a besoin d’un daemon, d’un socket, de droits root… bref, tout ce qu’on veut éviter.

Oui… Docker a un mode “rootless” merci, je sais. Mais il va quand même vous demander un daemon, un socket… Et regardez la méthode pour le mettre en place, je vous laisse juger.

Du coup, vous testez DinD (Docker in Docker), et après quelques sueurs, ça fonctionne. À peu près.

Google tente de nous sauver avec Kaniko : pas besoin de daemon, ça build des images OCI comme un chef. On y croit. Même Gitlab le propose dans sa documentation, on se dit que ça y est, on est soulagé.

Et là… pouf, projet archivé. Plus de support. Rideau.

Résultat : tout le monde retourne chez BuildX. Parce que Docker, c’est Docker, hein.

Sauf que BuildX, c’est un peu comme DinD en costard. Toujours dépendant du daemon, avec une stack à rallonge pour le faire tourner proprement en CI. Alors oui, il pourrait fonctionner sans daemon local, mais je vous passe les détails, c’est un setup demandant des compétences assez avancées.

Spoiler : Buildah fait ça sans broncher, et sans daemon foireux à configurer.

Podman, un début de réponse

Pendant que Docker et ses potes foutent le zbeul en CI/CD, y’a un petit héros discret qui traîne dans mon terminal depuis des années : Podman.

Podman

C’est clairement mon remplaçant préféré de Docker pour bosser en local. Les commandes sont quasiment les mêmes (podman run, podman build, tout ça), donc pas besoin de réapprendre l’alphabet. Mais sous le capot, c’est une autre histoire.

Alors, en vrai, depuis un bon moment, pour moi c’est Docker qui est une alternative à Podman. Sans rire, je trouve Docker trop lourd, moins bien pensé, voire problématique. Sans méchanceté, je préfère Podman.

Il m’a supprimé une grosse charge mentale…

Parce qu’il est daemonless — pas de service qui tourne en arrière-plan à l’insu de mon plein gré — et rootless — pas besoin de lui filer les clés de l’appart pour retrouver la vaiselles dégeux dans l’éviers (et je ne vous parle pas de l’état des chiottes).

Cerise sur le conteneur : il fonctionne dans votre répertoire utilisateur, avec votre ID, votre user, vos petits droits propres. Il construit des images OCI aussi bien que Docker (ou plutôt comme Kaniko avant son décès), compatibles avec Docker / RunC / Nerdctl, etc.

Mais voilà le twist : Podman, c’est surtout pour le développeur ou l’utilisateur qui veut faire tourner des machins sur son poste. Il est parfait pour lancer un conteneur vite fait pendant que vous tisez votre café, ou avec un fichier “compose” (à la docker-compose), voire utiliser des Quadlets (bon sang que j’aime ce truc). Mais il n’est pas réellement pensé pour des pipelines CI/CD à la chaîne.

Même s’il le peut, ce ne serait pas une vraie bonne idée.

L’astuce ? Le vrai cerveau de la construction d’image derrière Podman, c’est pas Podman lui-même. C’est son binôme discret : Buildah.

Et lui, on va en reparler…

Buildah, la solution

Le “builder” d’image sous-jacent de Podman, c’est Buildah.

Buildah logo

Buildah est, en gros, la sous-partie utilisable pour créer des images. Et vous allez voir qu’il propose des fonctionnalités avancées qui peuvent vraiment vous intéresser.

En gros, quand vous faites podman build, vous faites l’équivalent de buildah build. On peut donc ne pas installer Podman et n’utiliser que Buildah si notre but est de seulement construire des images et les pousser sur un registre.

Buildah peut fonctionner selon deux modes :

  • avec un fichier Containerfile (ou Dockerfile), via buildah build (ce qui est la méthode la plus courrante, proche de ce que vous connaissez déjà)
  • de manière programmatique, en utilisant son API et donc de scripter une création d’image avec des conditions fines

La construction simple

Amusons-nous et créons un fichier /tmp/monimage/Containerfile et dedans, vous mettez :

FROM alpine:latest

RUN apk add --no-cache nginx
CMD ["nginx", "-g", "daemon off;"]

Et tapez la commande suivante :

buildah build -t mon-image:latest /tmp/monimage

Vous le voyez, c’est super simple. La commande est typiquement la même que pour Docker ou Podman.

Dans certaines documentations, vous allez voir “bud” au lieu de “build”. C’est la même chose, c’est juste un alias. En fait la commande historique était buildah bud pour “build using Dockerfile”. Mais depuis un certain temps, la commande “buildah --help” ne liste que “build” bien que l’alias “bud” existe encore.

Tentons de faire la même chose dans un conteneur. Vous avez besoin de “capabilities” pour taper dans /proc alors, on va utiliser --privileged pour l’occasion, mais on pourrait tout à fait utiliser des “capabilities” plus fines.

# avec podman ou docker, vous pouvez faire
podman run --rm -it --privileged \
  -v /tmp/monimage:/tmp/monimage:z \
  fedora:42

Maintenant, dans le conteneur, installez Buildah et faite un “build” de l’image :

dnf install buildah -y
# On force le driver de stockage à vfs
export STORAGE_DRIVER=vfs

# puis
buildah build -t mon-image:latest /tmp/monimage

On force Buildah à utiliser le driver “vfs” car dans un conteneur, FUSE posera des problèmes. Il a besoin de modules pour faire des points de montages, et nous avons des restrictions. VFS est un poil plus lent, mais il fonctionne très bien dans un conteneur.

Ça a marché. Sans monter de socket Docker/Podman, sans lancer de service, sans rien faire de spécial. Buildah a simplement construit l’image OCI. C’est net.

Petite note avec les distributions Debian like, genre Ubuntu, bien entendu, ils font les choses mal (oui, je trolle) et vous avez des étapes supplémentaires à faire…

apt update && apt upgrade ca-certificates -y && apt install buildah -y
# puis, faire le reste
export STORAGE_DRIVER=vfs
buildah build -t mon-image:latest /tmp/monimage

Ce n’est pas insurmontable, mais sans la mise à jour de “ca-certificates”, vous allez avoir des erreurs de validation des certificats pour récupérer les images de base.

Le scripting avec Buildah

Mais là où Buildah devient intéressant, c’est qu’il permet de construire des images OCI de manière programmatique. Vous pouvez effectuer les étapes de build les une après les autres, et agir en fonction de vos besoins. Donc, entendez bien ce que je dis, vous pouvez éviter d’utiliser un Dockerfile ou un Containerfile et utiliser un script Bash. Faisons la même image que précédemment, mais en scriptant la construction.

Toujours dans un conteneur Alpine, avec Buildah installé, vous pouvez faire :

# dans un conteneur
export STORAGE_DRIVER=vfs

# on construit étape par étape
container=$(buildah from alpine:latest)
buildah run $container apk add --no-cache nginx
buildah config --cmd 'nginx -g "daemon off;"' $container
buildah config --port 80 $container
buildah config --port 443 $container
buildah commit $container mon-image:latest

Alors ATTENTION, ne vous méprenez pas. Le “run” de Buildah n’est pas le “run” de Docker ou Podman. Il ne lance pas un conteneur mais effectue l’opération “RUN” d’un Containerfile / Dockerfile. Je vous rappelle que Buildah est un outil de construction d’image OCI, pas un outil de gestion de conteneurs.

Je ne sais pas si vous réalisez l’intérêt de ce genre de construction. Imaginez par exemple que je veuille créer une image en fonction de paramètres extérieurs, ajouter des labels depuis des résultats de commandes… tiens par exemple :

container=$(buildah from alpine:latest)
buildah run $container apk add --no-cache nginx
buildah config --cmd 'nginx -g "daemon off;"' $container
# on ajoute un label pour se souvenir de l'envirinnement de build
# avec uname, date etc:
buildah config --label "build.env=$(uname -a)" $container
buildah config --label "build.date=$(date)" $container

buildah commit $container mon-image:latest

Vous allez pouvoir faire des scripts qui vont construire des images de manière conditionnelle, en fonction de configuration, d’environnement, de paramètres passés en ligne de commande, etc.

Alors effectivement, en règle générale, on préfèrera utiliser des Containerfile, mais le simple fait de pouvoir descendre dans les couches d’abstraction vous confère un pouvoir et de grandes possibilités.

Expérimenter dans une Action GitHub

Avant de vous parler des deux actions de Red Hat, on peut s’amuser à tester si Buildah fonctionne sans trop se taper la tête contre le clavier.

On va donc créer un petit test. J’ai un simple Containerfile dans un dépôt GitHub qui part de “nginx”, dépose un fichier index dans le répertoire /usr/share/nginx/html/. Ça suffit amplement.

FROM docker.io/nginx

COPY index.html /usr/share/nginx/html/index.html

Maintenant, dans .github/workflows/build-oci.yml, on va faire un petit test :

name: Build image

on: workflow_dispatch

# pour la suite
env:
  IMAGE_NAME: ${{ github.repository }}
  REGISTRY: ghcr.io

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Install Buildah
        run: |-
          sudo apt-get update
          sudo apt-get install -y buildah
      - name: Buildah
        run: |-
          buildah build -t $REGISTRY/${IMAGE_NAME} -f ./oci/Containerfile .

L’événement workflow_dispatch permet de lancer le workflow manuellement depuis l’interface de GitHub. C’est plus simple pour tester deux trois choses.

Bref, voilà le résultat :

# ...
STEP 1/2: FROM docker.io/nginx
Trying to pull docker.io/library/nginx:latest...
Getting image source signatures
Copying blob sha256:1d9f51194194364d2bbc948f82bc5161b65f277c6da4b0e47764e4fee2136e42
Copying blob sha256:140da4f89dcb7e49c2985bdd85b5c1c4e800916ee22abf7ada6b0bc2a99642a2
Copying blob sha256:59e22667830bf04fb35e15ed9c70023e9d121719bb87f0db7f3159ee7c7e0b8d
Copying blob sha256:2ef442a3816e24347655167c51e8d4fd033bf7da8f954f2f1cfd456cf6a06a80
Copying blob sha256:96e47e70491e3d5a8a65384c656bf4db13930a353c92252338e1167dfb1aeb84
Copying blob sha256:4b1e45a9989f25a049b0eace36b5405e64b4c2a31de65a92abfd86dbfb2b4a78
Copying blob sha256:f30ffbee4c546383f6ad349c50f8a2d2db5a83292489bbae5284b425960bcd42
Copying config sha256:2cd1d97f893f70cee86a38b7160c30e5750f3ed6ad86c598884ca9c6a563a501
Writing manifest to image destination
STEP 2/2: COPY index.html /usr/share/nginx/html/index.html
COMMIT imagetest
Getting image source signatures
Copying blob sha256:7cc7fe68eff66f19872441a51938eecc4ad33746d2baa3abc081c1e6fe25988e
Copying blob sha256:30837a0774b97fd5fd6306e4a67b9dba10fff174d82f0e7d2c6104dc79716fc3
Copying blob sha256:a6b19c3d00b104e9f5ae798917606b4706bf27748a008a5b44211b801ede45d8
Copying blob sha256:6b1b97dc92853f9880a37c378d2fe582eaca6567f3962d6d69dcbf35f65d95f9
Copying blob sha256:5c91a024d899eb09709296029b21f98722fa446ac7e37bb85b36f6484c499a2e
Copying blob sha256:0662742b23b26cd3b0c231ac4003d7343e3df50c62765b0baf6336c58156fa76
Copying blob sha256:f17478b6e8f3dcfdeb55c9e82529529ff6a984b3f72daa2fd723435f9ee9f93f
Copying blob sha256:6912176a43ddce467ba1cb2897ad73b6dbc97706370c1ce5ce9c55a8f439d06a
Copying config sha256:e3438ea014a5d5026f692e244b6190f53aeba0e0ea9604fcc8edf55e3da53c93
Writing manifest to image destination
--> e3438ea014a5
Successfully tagged localhost/imagetest:latest
e3438ea014a5d5026f692e244b6190f53aeba0e0ea9604fcc8edf55e3da53c93

C’est tout bonnement OK.

Et si vous voulez utiliser le registre GitHub de votre projet, ajoutez :

buildah login $REGISTRY -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
buildah push $REGISTRY/${IMAGE_NAME}

Bien entendu, vous pouvez le faire dans un “step” séparé. Aussi, pensez à {{github.ref_name}} pour tagger les images, ajoutez les labels pour avoir une description, etc.

Et donc, par conséquent, on peut se passer de Containerfile:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      packages: write
      contents: read
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Install Buildah
        run: |-
          sudo apt-get update
          sudo apt-get install -y buildah
      - name: Buildah
        run: |-
          container=$(buildah from alpine:latest)
          buildah run $container apk add --no-cache nginx
          buildah copy $container index.html /usr/share/nginx/html/index.html
          buildah config --port 80 $container
          buildah config --port 443 $container
          buildah config --cmd 'nginx -g "daemon off;"' $container

Bref, Buildah est un outil puissant et flexible pour construire des images OCI, que ce soit avec un Containerfile ou de manière programmatique. Il est idéal pour les environnements CI/CD, car il ne nécessite pas de service en arrière-plan et peut être utilisé dans des conteneurs sans problème de sécurité.

GitHub Actions Red Hat

Red Hat vous propose deux actions pour faire du build d’image OCI avec Buildah.

Ce qui veut dire que votre workflow n’aura besoin que de deux étapes :

  • build de l’image
  • push de l’image

Ce qui va ressembler à ça:

steps:
  - name: Checkout repository
    uses: actions/checkout@v4
  - name: Build image
    id: build-image
    uses: redhat-actions/buildah-build@v2
    with:
      image: ${{ env.REGISTRY}}/${{ env.IMAGE_NAME }}
      tags: latest ${{ github.ref_name }}
      containerfiles: |
        ./oci/Containerfile
  - name: Push image
    uses: redhat-actions/push-to-registry@v2
    with:
      image: ${{ steps.build-image.outputs.image }}
      tags: ${{ steps.build-image.outputs.tags }}
      registry: ${{ env.REGISTRY }}
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

Comparaison avec BuidX à la sauce Github…

On nous propose :

  • une action de login vers le registre
  • une action pour installer Qemu (ha ?)
  • une action pour installer BuildX (bah ouais)
  • une action “build and push”

En d’autres termes, avec BuildX à la sauce GitHub, ça va ressembler à ça :

name: Build OCI image
on:
  release:
    types:
      - published
  push:
    branches:
      - "features/**"
env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Log in to the Container registry
        uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./oci/Containerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

Ça ne parait pas énorme à première vue, mais regardez bien les étapes. On passe de 2 étapes à 4 étapes. Des setups assez lourds, et un build bien plus long. Rappelez-vous que, plus vous ajoutez de “steps”, plus vous risquez un plantage du setup, et plus ce sera difficile à corriger.

Avec Buildah, vous pouvez le faire à la main, ou utiliser les actions de Red Hat, dans les deux cas c’est plus souple, plus léger et plus facile à adapter. J’aime éviter l’accumulation de couches trop abstraites.

Je fais partie des gens qui estiment qu’il est souvent plus simple de faire un truc soi-même que de se reposer sur des intégrations obscures, à la limite de la boite noire, et de passer des heures à faire des diagnostics pour comprendre qu’il manquait une option perdue dans les bas-fonds d’une doc mal écrite. On sait coder bordel, on sait scripter. Arrêtez de vous laisser bouffer par des surcouches qui vous promettent la lune.

Le multi-arch, le point sensible

Buildah sait très bien gérer le build multi-arch. Cependant, il faut l’admettre, c’est un point un peu plus technique et moins évident à comprendre.

Pour préciser, un build “multi-arch” permet de construire une image qui peut être utilisée sur plusieurs architectures (arm64, amd64, etc.). C’est essentiel pour les environnements modernes où les conteneurs peuvent tourner sur des machines ayant différents types de microprocesseurs.

Les registres OCI/Docker en version 2.2 savent bien différencier ces architectures pour une image donnée. Pour cela, il faut créer un manifest et indiquer les images à utiliser. Docker rend ce truc un peu plus simple. Je l’avoue sans mal. Pour autant, quand on a compris l’astuce, ça parait évident avec Buildah.

Faisons deux images :

# image de base AMD64
buildah build -t mon-image:latest-amd64 --arch amd64 /tmp/monimage

# image ARM64
buildah build -t mon-image:latest-arm64 --arch arm64 /tmp/monimage

On peut pousser ces images telles quelles dans un registre, mais pour simplifier la vie des utilisateurs, on va créer un “manifest” qui va regrouper ces deux images. Pour cela, on va utiliser la commande buildah manifest.

buildah manifest create mon-image:multi-arch
buildah manifest add mon-image:multi-arch mon-image:latest-amd64
buildah manifest add mon-image:multi-arch mon-image:latest-arm64

# pour pousser le manufest
buildah manifest push --all mon-image:multi-arch $REGISTRY/$IMAGE_NAME:latest

Si maintenant un utilisateur lance une de ces commandes :

# avec docker
docker pull $REGISTRY/$IMAGE_NAME:latest
# avec podman
podman pull $REGISTRY/$IMAGE_NAME:latest
# ou encore avec nerdctl
nerdctl pull $REGISTRY/$IMAGE_NAME:latest

Alors, l’image adaptée pour son CPU sera automatiquement récupérée. C’est le principe du manifest OCI.

C’est ce poit qui est un peu douloureux, Buildah vous demande de faire des choses à la main. Ce n’est pas insurmontable, mais il faut l’avouer, c’est moins simple qu’avec Docker.

Et Gitlab ? Ils ont déjà fait le job

Si vous jetez un œil sur cette page de documentation, vous allez voir que GitLab a déjà fait le job pour vous.

Vous remarquerez qu’ils proposent d’utiliser l’image quay.io/buildah/stable qui a déjà tout le bazar pour utiliser les outils Buildah. Et pour parfaire le tout, cette image contient aussi les outils pour faire du “multi-arch”. Chose que fait très bien Buildah.

Quand vous comparer Buildah avec la page de doc pour Docker ou celle pour utiliser BuildKit, vous êtes en droit de vous demander pourquoi vous continuez à vous faire autant de mal.

Mais je vous l’avoue, je me suis surtout penché sur GitHub pour le moment.

Conclusion

J’ai du mal à comprendre pourquoi BuildX est autant mis en avant. Je suis conscient que Docker est roi, que Podman n’a pas la force de frappe en termes de communication, et que les vieilles habitudes sont dures à abandonner.

Mais, nous sommes en 2025, le monde du DevOps est de qualité, les ingénieurs sont ultra-compétents et adorent fouiller et tester. Podman (et notamment la version Desktop qui plait énormément sur Windows) est un outil fantastique. Buildah fonctionne du tonnerre. Ces deux outils simplifient notre travail, ils soulagent les machines, ils font le job comme on l’entend.

Je pense, en toute honnêteté, que Buildah devrait être l’outil par défaut dans les pipelines CI/CD.

Je l’ai déjà dit sur différents réseaux sociaux, j’admire le travaille effectué par Docker, ils ont révolutionné le monde de l’informatique en proposant un système de conteneurs simple et efficace. Docker a changé la donne, a modifié les paradigmes et permis l’avènement de Kubernetes. Cependant… Docker se concentre sur les postes de travail, sur le hub et sur sa version Desktop sous licence. C’est un choix assumé et je le respecte. Mais, nous, ingénieurs DevOps, nous devons nous concentrer sur l’efficacité, la sécurité et la simplicité.

Buildah répond à ces conditions.

Quant à Podman, devenu l’outil par défaut pour Fedora / RockyLinux / Alma / Red Hat, il est temps que vous y prêtiez attention si vous êtes développeurs.

comments powered by Disqus