Aller au contenu
Conteneurs & Orchestration medium

Construire des images OCI sans démon avec Buildah

28 min de lecture

logo buildah

Buildah construit des images de conteneurs OCI sans démon Docker et en mode rootless. Créé par Red Hat en 2017, cet outil permet de builder des images avec de simples commandes shell (from, run, commit) ou via Dockerfiles (commande bud). Contrairement à docker build, Buildah n’exige aucun processus en arrière-plan et peut s’exécuter avec un utilisateur non privilégié — idéal pour sécuriser vos pipelines CI/CD.

Ce que vous allez apprendre :

  • Pourquoi choisir Buildah : mode rootless, absence de démon, sécurité renforcée
  • Construction from scratch : workflow fromruncommitpush
  • Construction avec Dockerfile : commande bud (build-using-dockerfile)
  • Intégration CI/CD : GitLab CI et GitHub Actions avec Buildah
  • Bonnes pratiques : multi-stage builds, cache, attestations

Prérequis : Linux (Ubuntu/Fedora/RHEL), connaissances de base en Dockerfile et conteneurs.

Écosystème Buildah

docker build nécessite un démon Docker root en permanence :

Fenêtre de terminal
# Docker build classique
docker build -t myapp:1.0.0 .
# → Envoie le contexte au démon Docker (root)
# → Le démon construit l'image
# → Privilèges root requis

Inconvénients :

  • Démon root permanent : surface d’attaque élevée (CVE-2019-5736, CVE-2025-49713)
  • Privilèges élevés : tout utilisateur du groupe docker = root effectif
  • CI/CD complexe : montage du socket Docker (/var/run/docker.sock) dans les conteneurs
  • Rootless incomplet : docker build rootless reste expérimental et limité

Buildah élimine le démon et fonctionne directement avec les images OCI :

Fenêtre de terminal
# Buildah : pas de démon
buildah from alpine:3.21 # Créer conteneur temporaire
buildah run <ID> apk add nginx # Installer paquets
buildah commit <ID> myapp:1.0.0 # Créer image OCI
# → Aucun démon, exécutable en tant qu'utilisateur

Gains immédiats :

CritèreDocker BuildBuildah
Démon requis✅ Oui (root)❌ Non
Rootless natif⚠️ Expérimental✅ Stable
Surface d’attaqueÉlevée (démon + socket)Faible (CLI directe)
Scriptable❌ Dockerfile uniquement✅ from/run/commit + Dockerfile
CI/CDDocker-in-Docker (DinD) ou socket mountImage Buildah standalone

Comparaison architectures

Le choix entre Buildah et Docker build dépend principalement de vos contraintes de sécurité et de votre environnement d’exécution. Buildah excelle dans les environnements où la sécurité est prioritaire (CI/CD, Kubernetes) grâce à son architecture rootless, tandis que Docker build reste pertinent pour le développement local avec des scripts existants.

SituationOutil recommandéRaison
CI/CD sécurisé (GitLab, GitHub Actions)BuildahPas de socket Docker, rootless stable
Kubernetes (build in-cluster)BuildahPas de démon, pods non privilégiés
Builds scriptés complexesBuildahfrom/run/commit = shell natif
Développement local avec DockerfilesDocker build ou Buildah budLes deux fonctionnent, Buildah = rootless
Compatibilité legacy (scripts existants)Docker buildMoins de refactoring
  1. Installer Buildah (Ubuntu/Debian)

    Fenêtre de terminal
    sudo apt update
    sudo apt install buildah

    Autres distributions :

    Fenêtre de terminal
    sudo dnf install buildah
  2. Vérifier l’installation

    Fenêtre de terminal
    buildah --version

    Sortie attendue (version 1.42.0 ou supérieure) :

    buildah version 1.42.0 (image-spec 1.2.0, runtime-spec 1.2.0)
  3. Configurer le mode rootless (recommandé)

    Buildah fonctionne en mode rootless grâce aux user namespaces. Vérifiez que votre utilisateur a les plages UID/GID configurées :

    Fenêtre de terminal
    cat /etc/subuid
    # bob:100000:65536
    cat /etc/subgid
    # bob:100000:65536

    Si les fichiers sont vides, ajoutez votre utilisateur :

    Fenêtre de terminal
    sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER

    Vérifier le mode rootless :

    Fenêtre de terminal
    buildah info | grep rootless
    # rootless: true
  4. Configurer le stockage (optionnel)

    Par défaut, Buildah stocke les images dans ~/.local/share/containers/storage. Pour changer :

    Fenêtre de terminal
    mkdir -p ~/.config/containers
    cat > ~/.config/containers/storage.conf <<EOF
    [storage]
    driver = "overlay"
    graphroot = "/home/$USER/buildah-storage"
    EOF

Le mode from scratch est la signature de Buildah : construire des images avec des commandes shell simples, sans Dockerfile.

Workflow from scratch

  1. Créer un conteneur de travail

    Fenêtre de terminal
    buildah from alpine:3.21

    Sortie :

    alpine-working-container

    Buildah crée un conteneur temporaire (nom généré automatiquement). Récupérez son ID :

    Fenêtre de terminal
    buildah containers

    Sortie :

    CONTAINER ID BUILDER IMAGE ID IMAGE NAME CONTAINER NAME
    ea12f417f4f4 * 39477eca89b9 docker.io/library/alpine:3.21 alpine-working-container
  2. Modifier le conteneur avec run

    Fenêtre de terminal
    buildah run alpine-working-container apk add --no-cache nginx

    Sortie :

    fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/APKINDEX.tar.gz
    fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/community/x86_64/APKINDEX.tar.gz
    (1/2) Installing pcre (8.45-r3)
    (2/2) Installing nginx (1.26.3-r0)
    Executing nginx-1.26.3-r0.pre-install
    Executing nginx-1.26.3-r0.post-install
    Executing busybox-1.37.0-r13.trigger
    OK: 8 MiB in 17 packages

    Commandes disponibles : toutes les commandes shell standard (apk, apt, yum, cp, mkdir, etc.)

  3. Configurer l’image (metadata)

    Fenêtre de terminal
    # Définir le CMD
    buildah config --cmd "nginx -g 'daemon off;'" alpine-working-container
    # Ajouter des labels OCI
    buildah config \
    --label "org.opencontainers.image.source=https://github.com/me/app" \
    --label "org.opencontainers.image.version=1.0.0" \
    alpine-working-container
    # Exposer un port (documentation)
    buildah config --port 80 alpine-working-container
  4. Créer l’image finale

    Fenêtre de terminal
    buildah commit alpine-working-container myapp-nginx:1.0.0

    Sortie :

    Getting image source signatures
    Copying blob sha256:922ec217407c0fd31cb18b46090bf62e439fb53ecd01f09406d62e25a906e09b
    Copying blob sha256:76544e3ee9871ab833c328242c056a68d37558048e2b842ff2e119ff6cedae08
    Copying config sha256:a85c85cb198dec139803c48618a0f96e0af53780736b5586dc4bde71efe35d38
    Writing manifest to image destination
    a85c85cb198dec139803c48618a0f96e0af53780736b5586dc4bde71efe35d38
  5. Vérifier l’image créée

    Fenêtre de terminal
    buildah images

    Sortie :

    REPOSITORY TAG IMAGE ID CREATED SIZE
    localhost/myapp-nginx 1.0.0 6d9817bc292f 2 minutes ago 9.63 MB
  6. Nettoyer le conteneur temporaire

    Fenêtre de terminal
    buildah rm alpine-working-container

Exemple de script build-nginx.sh pour automatiser le workflow :

#!/bin/bash
set -e
VERSION="1.0.0"
IMAGE_NAME="myapp-nginx"
# 1. Créer conteneur
container=$(buildah from alpine:3.21)
# 2. Installer paquets
buildah run "$container" apk add --no-cache nginx
# 3. Configurer
buildah config --cmd "nginx -g 'daemon off;'" "$container"
buildah config --port 80 "$container"
buildah config --label "version=$VERSION" "$container"
# 4. Créer image
buildah commit "$container" "$IMAGE_NAME:$VERSION"
# 5. Nettoyer
buildah rm "$container"
echo "✅ Image $IMAGE_NAME:$VERSION créée avec succès"

Exécution :

Fenêtre de terminal
chmod +x build-nginx.sh
./build-nginx.sh

Buildah supporte les Dockerfiles via la commande bud (Build Using Dockerfile) — alias de buildah build.

FROM alpine:3.21
# Installer nginx
RUN apk add --no-cache nginx
# Configurer
RUN mkdir -p /run/nginx
# Metadata
LABEL org.opencontainers.image.source="https://github.com/me/app"
LABEL org.opencontainers.image.version="1.0.0"
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Fenêtre de terminal
buildah bud -f Dockerfile -t myapp-nginx:1.0.0 .

Sortie :

[1/2] STEP 1/6: FROM alpine:3.21
Resolved "alpine" as an alias (/etc/containers/registries.conf.d/shortnames.conf)
Trying to pull docker.io/library/alpine:3.21...
Getting image source signatures
Copying blob 96526aa774ef done
Copying config 8ca4688f4f done
Writing manifest to image destination
[1/2] STEP 2/6: RUN apk add --no-cache nginx
fetch https://dl-cdn.alpinelinux.org/alpine/v3.21/main/x86_64/APKINDEX.tar.gz
(1/5) Installing nginx (1.24.0-r15)
OK: 12 MiB in 20 packages
[1/2] STEP 3/6: RUN mkdir -p /run/nginx
[1/2] STEP 4/6: LABEL org.opencontainers.image.source="https://github.com/me/app"
[1/2] STEP 5/6: LABEL org.opencontainers.image.version="1.0.0"
[1/2] STEP 6/6: CMD ["nginx", "-g", "daemon off;"]
[1/2] COMMIT myapp-nginx:1.0.0
Getting image source signatures
Copying blob cc2447e1835a skipped: already exists
Copying blob 5f70bf18a086 done
Copying config 2e05f739c6 done
Writing manifest to image destination
Storing signatures
--> 2e05f739c67
Successfully tagged localhost/myapp-nginx:1.0.0
2e05f739c674c1050c1700cf9d889b157c79c23c3f2fb4e066e044922c57402b

La commande bud accepte de nombreuses options pour personnaliser vos builds. Les plus utiles permettent de cibler une étape spécifique dans un multi-stage build (--target), de passer des arguments dynamiques (--build-arg), de forcer la reconstruction sans cache (--no-cache), ou de construire pour plusieurs architectures simultanément (--platform).

Fenêtre de terminal
# Multi-stage build
buildah bud --target production -t myapp:1.0.0 .
# Build args
buildah bud --build-arg VERSION=1.0.0 -t myapp:1.0.0 .
# Désactiver le cache
buildah bud --no-cache -t myapp:1.0.0 .
# Multi-platform (nécessite QEMU)
buildah bud --platform linux/amd64,linux/arm64 -t myapp:1.0.0 .
# Sortie directe vers registry
buildah bud --manifest myapp:1.0.0 \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/me/myapp:1.0.0 .
buildah manifest push --all myapp:1.0.0 docker://ghcr.io/me/myapp:1.0.0

Buildah utilise un système de transports pour gérer la destination des images. Cette approche flexible permet de pousser vers des registries OCI distants, de charger dans un démon Docker local pour des tests, ou d’exporter en fichiers pour la distribution offline. Le transport le plus courant est docker:// pour les registries standards (GHCR, Docker Hub, Harbor).

TransportSyntaxeUsage
docker://docker://ghcr.io/me/app:1.0.0Registry OCI (Docker Hub, GHCR, Harbor)
docker-daemon:docker-daemon:myapp:1.0.0Charger dans le démon Docker local
oci-archive:oci-archive:/tmp/myapp.tarFichier tar OCI
dir:dir:/tmp/myapp-dirDossier avec layout OCI
containers-storage:containers-storage:myapp:1.0.0Stockage Podman/Buildah local

Exemples :

Fenêtre de terminal
# Authentification
echo $GITHUB_TOKEN | buildah login ghcr.io -u $GITHUB_USER --password-stdin
# Push
buildah push myapp-nginx:1.0.0 docker://ghcr.io/$GITHUB_USER/myapp-nginx:1.0.0

Buildah offre plusieurs commandes pour analyser vos images : lister toutes les images disponibles, inspecter les métadonnées OCI complètes (labels, environnement, entrypoint), ou examiner la structure des layers pour comprendre la composition de l’image.

Fenêtre de terminal
# Lister toutes les images
buildah images
# Inspecter les métadonnées
buildah inspect --type image myapp-nginx:1.0.0
# Voir les layers
buildah inspect --format '{{.Docker.RootFS.Layers}}' myapp-nginx:1.0.0

Le nettoyage régulier des images inutilisées libère de l’espace disque et facilite la gestion. Buildah propose trois niveaux de nettoyage : suppression ciblée d’une image spécifique, pruning des images non utilisées, ou nettoyage complet incluant conteneurs et cache.

Fenêtre de terminal
# Supprimer une image
buildah rmi myapp-nginx:1.0.0
# Supprimer toutes les images non utilisées
buildah rmi --prune
# Nettoyer complètement (images + conteneurs + cache)
buildah system prune -a

Exemple .gitlab-ci.yml avec Buildah rootless :

variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE
IMAGE_TAG: $CI_COMMIT_SHORT_SHA
stages:
- build
- push
build-image:
stage: build
image: quay.io/buildah/stable:v1.42.0
script:
# Configurer rootless
- buildah info | grep rootless
# Build
- buildah bud -f Dockerfile -t $IMAGE_NAME:$IMAGE_TAG .
# Vérifier
- buildah images
artifacts:
reports:
# Optionnel : générer SBOM
cyclonedx: buildah-sbom.json
push-image:
stage: push
image: quay.io/buildah/stable:v1.42.0
dependencies:
- build-image
script:
# Authentification
- echo $CI_REGISTRY_PASSWORD | buildah login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
# Push
- buildah push $IMAGE_NAME:$IMAGE_TAG docker://$IMAGE_NAME:$IMAGE_TAG
# Tag latest (si main)
- |
if [ "$CI_COMMIT_BRANCH" == "main" ]; then
buildah tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
buildah push $IMAGE_NAME:latest docker://$IMAGE_NAME:latest
fi
only:
- main
- tags

Exemple .github/workflows/build-image.yml :

name: Build Image with Buildah
on:
push:
branches: [main]
pull_request:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- uses: actions/checkout@v4.2.2
- name: Install Buildah
run: |
sudo apt update
sudo apt install -y buildah
- name: Verify rootless
run: buildah info | grep "rootless: true"
- name: Build image
run: |
buildah bud \
-f Dockerfile \
-t $REGISTRY/$IMAGE_NAME:${{ github.sha }} \
-t $REGISTRY/$IMAGE_NAME:latest \
.
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
run: |
echo ${{ secrets.GITHUB_TOKEN }} | buildah login $REGISTRY -u ${{ github.actor }} --password-stdin
- name: Push image
if: github.event_name != 'pull_request'
run: |
buildah push $REGISTRY/$IMAGE_NAME:${{ github.sha }} docker://$REGISTRY/$IMAGE_NAME:${{ github.sha }}
buildah push $REGISTRY/$IMAGE_NAME:latest docker://$REGISTRY/$IMAGE_NAME:latest
- name: Generate SBOM (optionnel)
if: github.event_name != 'pull_request'
run: |
buildah inspect --format '{{.OCIv1.Config}}' $REGISTRY/$IMAGE_NAME:${{ github.sha }} > sbom.json
- name: Attest image (GitHub)
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v2.1.0
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

Exemple de Task Tekton :

apiVersion: tekton.dev/v1
kind: Task
metadata:
name: buildah-build
spec:
params:
- name: IMAGE
description: Image de sortie (ex: ghcr.io/me/app:1.0.0)
- name: DOCKERFILE
default: Dockerfile
- name: CONTEXT
default: .
workspaces:
- name: source
steps:
- name: build
image: quay.io/buildah/stable:v1.42.0
workingDir: $(workspaces.source.path)
script: |
#!/bin/bash
set -e
buildah bud \
-f $(params.DOCKERFILE) \
-t $(params.IMAGE) \
$(params.CONTEXT)
securityContext:
runAsUser: 1000 # Rootless
runAsGroup: 1000
volumeMounts:
- name: varlibcontainers
mountPath: /var/lib/containers
- name: push
image: quay.io/buildah/stable:v1.42.0
script: |
#!/bin/bash
set -e
buildah push $(params.IMAGE) docker://$(params.IMAGE)
securityContext:
runAsUser: 1000
runAsGroup: 1000
volumes:
- name: varlibcontainers
emptyDir: {}

La plupart des erreurs Buildah proviennent d’une mauvaise configuration du mode rootless (user namespaces, plages UID/GID). Le tableau ci-dessous récapitule les symptômes les plus fréquents avec leur diagnostic et la solution rapide à appliquer.

SymptômeCause probableSolution
permission denied lors du buildRootless mal configuréVérifier /etc/subuid et /etc/subgid, relancer session utilisateur
error creating build containerPas assez d’UIDs mappésAugmenter la plage dans /etc/subuid (65536 minimum)
ERRO[0000] cannot find UID/GIDUser namespaces désactivésVérifier sysctl user.max_user_namespaces (doit être > 0)
storage driver not supportedOverlay2 non disponibleVérifier buildah info | grep graphDriverName, installer fuse-overlayfs
failed to resolve imageRegistry non accessibleVérifier /etc/containers/registries.conf, tester buildah pull alpine:3.21
Multi-platform build échoueQEMU binfmt non installéInstaller qemu-user-static : sudo apt install qemu-user-static

Avant de chercher une solution complexe, commencez toujours par vérifier la configuration de base : est-ce que le mode rootless est activé ? Les registries sont-ils accessibles ? Les user namespaces sont-ils correctement configurés ? Ces commandes de diagnostic vous donnent rapidement les informations essentielles.

Fenêtre de terminal
# Informations système Buildah
buildah info
# Vérifier le mode rootless
buildah info | grep rootless
# Lister les registries configurés
cat /etc/containers/registries.conf
# Tester la résolution d'image
buildah pull alpine:3.21
# Vérifier les user namespaces
cat /proc/sys/user/max_user_namespaces
# Doit être > 0 (typiquement 65536)
# Logs détaillés
buildah --log-level debug bud -t test:1.0.0 .

Sur certaines distributions (notamment Ubuntu Server), les user namespaces sont désactivés par défaut pour des raisons de sécurité. Si sysctl user.max_user_namespaces retourne 0, vous devez les activer manuellement. Cette configuration est indispensable pour le mode rootless de Buildah.

Fenêtre de terminal
# Vérifier l'état actuel
sysctl user.max_user_namespaces
# Activer temporairement
sudo sysctl user.max_user_namespaces=65536
# Activer au démarrage
echo "user.max_user_namespaces=65536" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

La sécurité des images de conteneurs commence dès le build. Ces quatre pratiques fondamentales réduisent drastiquement les risques : mode rootless (aucun privilège root), utilisateur non privilégié dans l’image finale, scan des vulnérabilités avant publication, et signature cryptographique pour garantir l’intégrité.

  1. Toujours utiliser rootless en production

    Fenêtre de terminal
    # Vérifier avant de builder
    buildah info | grep "rootless: true"
  2. Éviter les privilèges élevés dans les conteneurs

    FROM alpine:3.21
    RUN adduser -D -u 1000 appuser
    USER appuser
  3. Scanner les images avec Trivy

    Fenêtre de terminal
    buildah bud -t myapp:1.0.0 .
    trivy image --severity HIGH,CRITICAL myapp:1.0.0
  4. Signer les images avec Cosign

    Fenêtre de terminal
    # Build + push
    buildah bud -t ghcr.io/me/app:1.0.0 .
    buildah push ghcr.io/me/app:1.0.0 docker://ghcr.io/me/app:1.0.0
    # Signer
    cosign sign ghcr.io/me/app:1.0.0

Optimiser les builds Buildah améliore à la fois le temps de construction et la taille des images finales. Les multi-stage builds séparent l’environnement de compilation de l’environnement d’exécution, le cache de layers évite de reconstruire les étapes inchangées, et un bon .dockerignore réduit la taille du contexte envoyé au builder.

  1. Utiliser des multi-stage builds

    # Build stage
    FROM golang:1.23-alpine AS builder
    WORKDIR /app
    COPY . .
    RUN go build -o myapp
    # Runtime stage
    FROM alpine:3.21
    COPY --from=builder /app/myapp /usr/local/bin/
    CMD ["myapp"]
  2. Activer le cache de layers

    Fenêtre de terminal
    # Cache local (défaut)
    buildah bud -t myapp:1.0.0 .
    # Cache registry (CI/CD)
    buildah bud \
    --cache-to=type=registry,ref=ghcr.io/me/cache:myapp \
    --cache-from=type=registry,ref=ghcr.io/me/cache:myapp \
    -t myapp:1.0.0 .
  3. Optimiser le contexte de build

    Créer un .dockerignore :

    .git
    node_modules
    *.md
    tests/

Des builds reproductibles et traçables facilitent le debugging et la collaboration. Versionner explicitement les images de base évite les surprises lors des rebuilds, les labels OCI structurent les métadonnées (source, version, commit SHA), et centraliser le processus de build dans un script garantit la cohérence entre développeurs et CI/CD.

  1. Versionner les images de base

    # ❌ Mauvais (latest implicite)
    FROM alpine
    # ✅ Bon (version explicite)
    FROM alpine:3.21
  2. Ajouter des labels OCI

    LABEL org.opencontainers.image.source="https://github.com/me/app"
    LABEL org.opencontainers.image.version="1.0.0"
    LABEL org.opencontainers.image.revision="${GIT_SHA}"
    LABEL org.opencontainers.image.created="${BUILD_DATE}"
  3. Centraliser les builds dans un script

    build.sh
    #!/bin/bash
    set -e
    VERSION=${1:-dev}
    IMAGE="ghcr.io/me/app"
    echo "🔨 Building $IMAGE:$VERSION..."
    buildah bud -t "$IMAGE:$VERSION" .
    echo "🔍 Scanning image..."
    trivy image --severity HIGH,CRITICAL "$IMAGE:$VERSION"
    if [ "$VERSION" != "dev" ]; then
    echo "📤 Pushing to registry..."
    buildah push "$IMAGE:$VERSION" docker://"$IMAGE:$VERSION"
    fi
    echo "✅ Done!"
  1. Buildah = builds sans démon : aucun processus Docker root requis, CLI directe
  2. Rootless natif : mode par défaut, sécurité renforcée vs docker build
  3. Mettez à jour vers v1.42+ : correctifs CVE-2025-47913 et CVE-2025-47914
  4. Deux workflows : from/run/commit (scriptable) ou bud (Dockerfile)
  5. Transports multiples : docker://, docker-daemon:, oci-archive:, etc.
  6. CI/CD idéal : GitLab CI, GitHub Actions, Tekton — aucun socket Docker à monter
  7. Compatible OCI : images utilisables avec Docker, Podman, Kubernetes
  8. Bonnes pratiques : rootless + multi-stage + scan Trivy + labels OCI
  9. Dépannage : vérifier /etc/subuid, user.max_user_namespaces, buildah info

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.