
Ce guide vous apprend à construire une image reproductible, petite, rapide à builder, et à la publier proprement. Vous comprendrez le raisonnement derrière chaque décision de build, pas juste les options.
À la fin, vous saurez :
- Optimiser le cache pour des builds en quelques secondes
- Utiliser multi-stage pour diviser la taille par 10
- Passer des secrets sans les exposer dans l’image
- Publier vers un registry avec un digest vérifiable
Droit au but
Section intitulée « Droit au but »Modèle mental : comment podman build fonctionne
Section intitulée « Modèle mental : comment podman build fonctionne »Podman utilise Buildah en interne
Section intitulée « Podman utilise Buildah en interne »Quand vous exécutez podman build, Podman appelle Buildah pour construire l’image. Cela a des implications importantes :
| Aspect | Ce que ça signifie |
|---|---|
| Stockage partagé | podman et buildah voient les mêmes images |
| Compatibilité | Dockerfile et Containerfile sont équivalents |
| Options avancées | Certaines options viennent de Buildah (--secret, --ssh) |
| Sans démon | Pas de daemon à maintenir, builds rootless possibles |
Le contexte de build
Section intitulée « Le contexte de build »Le contexte est le dossier envoyé au builder. Tout ce qui est dedans peut être copié dans l’image avec COPY.
# Le "." à la fin = contexte = dossier courantpodman build -t mon-image:1.0 .Problème fréquent : un dossier node_modules/ de 500 MB dans le contexte ralentit tous les builds.
Solution : le fichier .containerignore (voir section cache).
Premier build reproductible
Section intitulée « Premier build reproductible »Un build reproductible signifie : même Containerfile → même image, demain comme dans 6 mois.
Règle 1 : pinner la version de base
Section intitulée « Règle 1 : pinner la version de base »# ❌ Mauvais : "latest" peut changer demainFROM python:latest
# ❌ Mauvais encore : "3.12" peut passer de 3.12.1 à 3.12.2FROM python:3.12-alpine
# ✅ Bon : version exacte + digest pour la reproductibilitéFROM python:3.12.8-alpine3.21Notre premier Containerfile
Section intitulée « Notre premier Containerfile »Répertoiremon-api/
- Containerfile
- requirements.txt
Répertoireapp/
- init .py
- main.py
FROM python:3.12.8-alpine3.21
LABEL org.opencontainers.image.title="Mon API"LABEL org.opencontainers.image.version="1.0.0"
WORKDIR /app
# Copier tout et installerCOPY . .RUN pip install --no-cache-dir -r requirements.txt
# Utilisateur non-rootRUN adduser -D appuserUSER appuser
EXPOSE 8000CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0"]Construire et mesurer
Section intitulée « Construire et mesurer »-
Construire l’image
Fenêtre de terminal time podman build -t mon-api:v1-naive .Notez le temps de build et la taille.
-
Mesurer la taille
Fenêtre de terminal podman images | grep mon-apiRésultat localhost/mon-api v1-naive abc123 30 seconds ago 287 MB -
Voir les layers
Fenêtre de terminal podman image history mon-api:v1-naive -
Tester
Fenêtre de terminal podman run --rm -p 8000:8000 mon-api:v1-naive# Dans un autre terminal: curl http://localhost:8000
Constat : 287 MB et 45 secondes de build. On peut faire beaucoup mieux.
Le vrai levier : le cache
Section intitulée « Le vrai levier : le cache »Le cache est l’optimisation la plus importante. Un build bien caché passe de 2 min à 10 sec.
Comment le cache fonctionne
Section intitulée « Comment le cache fonctionne »Podman met en cache chaque instruction (chaque RUN, COPY, etc. crée un “layer”). Si une instruction n’a pas changé, le layer en cache est réutilisé.
Mais : dès qu’un layer change, tous les layers suivants sont invalidés.
L’erreur classique : tout copier trop tôt
Section intitulée « L’erreur classique : tout copier trop tôt »# ❌ Cache invalidé à chaque modification de codeFROM python:3.12.8-alpine3.21WORKDIR /appCOPY . . # ← Change à chaque commitRUN pip install -r requirements.txt # ← Rebuild 100% du temps !La solution : séparer dépendances et code
Section intitulée « La solution : séparer dépendances et code »# ✅ Dépendances installées une seule foisFROM python:3.12.8-alpine3.21WORKDIR /app
# 1. Copier UNIQUEMENT les fichiers de dépendancesCOPY requirements.txt .
# 2. Installer (en cache tant que requirements.txt ne change pas)RUN pip install --no-cache-dir -r requirements.txt
# 3. Copier le code (change souvent, mais pip est déjà fait)COPY . .
CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0"]Mesurer l’impact
Section intitulée « Mesurer l’impact »# Premier build (cold cache)time podman build --no-cache -t mon-api:v2-cache .# → 45 secondes
# Modifier un fichier Pythonecho "# comment" >> app/main.py
# Deuxième build (warm cache)time podman build -t mon-api:v2-cache .# → 3 secondes ! (pip install est en cache)Le fichier .containerignore
Section intitulée « Le fichier .containerignore »Excluez du contexte tout ce qui n’est pas nécessaire au build :
# Contrôle de version.git/.gitignore
# Fichiers de développement*.mdREADME*docs/tests/
# Environnements virtuels (installés dans l'image).venv/venv/__pycache__/*.pyc
# Secrets et config locale.env.env.*secrets/*.key*.pem
# Fichiers de buildContainerfile*.containerignore.dockerignoreMesurer l’impact :
# Avant .containerignoredu -sh mon-api/# 500 MB (avec .venv et .git)
# Après .containerignore (effectif dans le build)tar -cvf - --exclude-from=.containerignore . | wc -c# 50 KBQuand utiliser —no-cache
Section intitulée « Quand utiliser —no-cache »--no-cache est un outil de diagnostic, pas un mode normal.
| Situation | Utilisez |
|---|---|
| Build de développement | Cache normal |
| CI/CD répétitif | Cache normal |
| ”Mon build semble buggé” | --no-cache pour debug |
| Image finale avant release | --no-cache (optionnel) |
Multi-stage : le standard
Section intitulée « Multi-stage : le standard »Le multi-stage build sépare l’environnement de compilation de l’environnement d’exécution. C’est le pattern standard pour les applications compilées.
Le problème : images obèses
Section intitulée « Le problème : images obèses »# Image de développement = tout installéFROM python:3.12-slim
RUN apt-get update && apt-get install -y \ gcc libpq-dev # Nécessaires au build seulement
COPY requirements.txt .RUN pip install -r requirements.txt
COPY . .CMD ["python", "app.py"]Taille : 400 MB (gcc et libpq-dev inclus, inutiles au runtime).
La solution : multi-stage
Section intitulée « La solution : multi-stage »# Stage 1: Builder (avec outils de compilation)FROM python:3.12.8-alpine3.21 AS builder
RUN apk add --no-cache gcc musl-dev libffi-dev
WORKDIR /installCOPY requirements.txt .
# Installer dans un prefix isoléRUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: Runtime (minimal)FROM python:3.12.8-alpine3.21 AS runtime
# Copier UNIQUEMENT les packages installésCOPY --from=builder /install /usr/local
WORKDIR /appCOPY app/ ./app/
# Utilisateur non-rootRUN adduser -D appuserUSER appuser
EXPOSE 8000CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0"]Construire et comparer
Section intitulée « Construire et comparer »# Build multi-stagepodman build -t mon-api:v3-multistage .
# Comparer les taillespodman images | grep mon-apilocalhost/mon-api v3-multistage def456 10 seconds ago 52 MBlocalhost/mon-api v1-naive abc123 30 seconds ago 287 MBGain : 82% de réduction (287 MB → 52 MB).
Cibler un stage spécifique
Section intitulée « Cibler un stage spécifique »Parfois vous voulez construire uniquement le stage builder (pour debug ou tests) :
# Construire uniquement le builderpodman build --target builder -t mon-api:builder .
# Le stage builder contient gcc, parfait pour les testspodman run --rm mon-api:builder python -m pytestExemple multi-stage Go (binaire statique)
Section intitulée « Exemple multi-stage Go (binaire statique) »# BuilderFROM golang:1.22-alpine AS builder
WORKDIR /buildCOPY go.mod go.sum ./RUN go mod download
COPY . .RUN CGO_ENABLED=0 go build -o /app
# Runtime (FROM scratch = 0 MB)FROM scratch
COPY --from=builder /app /appENTRYPOINT ["/app"]Taille finale : 8 MB (juste le binaire statique).
Sécurité : secrets au build
Section intitulée « Sécurité : secrets au build »Certains builds nécessitent des secrets : token npm, clé SSH pour un repo privé, credentials pip. Ne les mettez jamais en ARG ou ENV — ils seraient visibles dans l’historique de l’image.
Le problème : secrets brûlés dans l’image
Section intitulée « Le problème : secrets brûlés dans l’image »# ❌ DANGEREUX : le token est dans l'image finaleARG NPM_TOKENRUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrcRUN npm install# N'importe qui peut extraire le tokenpodman history mon-image --no-trunc | grep NPM_TOKENLa solution : —secret
Section intitulée « La solution : —secret »L’option --secret monte un fichier secret temporairement pendant le build, sans le persister dans l’image.
FROM node:20-alpine
WORKDIR /appCOPY package*.json ./
# Le secret est disponible UNIQUEMENT pendant ce RUNRUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm install
COPY . .CMD ["node", "index.js"]# Créer le fichier secretecho "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > /tmp/npmrc
# Build avec le secretpodman build --secret id=npmrc,src=/tmp/npmrc -t mon-app:secure .Vérification : le secret n’est pas dans l’image.
podman history mon-app:secure --no-trunc | grep -i token# (aucun résultat)Exemples de secrets au build
Section intitulée « Exemples de secrets au build »RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm installpodman build --secret id=npmrc,src=$HOME/.npmrc .RUN --mount=type=secret,id=pip,target=/root/.pip/pip.conf \ pip install -r requirements.txtpodman build --secret id=pip,src=$HOME/.pip/pip.conf .RUN --mount=type=ssh \ git clone git@github.com:private/repo.giteval $(ssh-agent)ssh-add ~/.ssh/id_ed25519podman build --ssh default .Formats d’image : OCI vs Docker
Section intitulée « Formats d’image : OCI vs Docker »Podman produit des images au format OCI par défaut. C’est le format standard, supporté par tous les registries modernes.
| Format | Default Podman | Compatible |
|---|---|---|
| OCI | ✅ | Tous registries modernes |
| Docker | Non | Legacy, Docker Hub |
Quand forcer un format
Section intitulée « Quand forcer un format »# Format OCI (défaut)podman build --format oci -t mon-image:oci .
# Format Docker (compatibilité legacy)podman build --format docker -t mon-image:docker .Règle : gardez OCI sauf si vous avez un problème de compatibilité spécifique.
Publier proprement
Section intitulée « Publier proprement »L’objectif : l’image est poussée vers un registry et vous avez conservé son digest (identifiant immuable).
Workflow complet
Section intitulée « Workflow complet »-
Tag sémantique
Fenêtre de terminal # Tag de développementpodman tag mon-api:v3-multistage mon-api:1.0.0podman tag mon-api:v3-multistage mon-api:1.0podman tag mon-api:v3-multistage mon-api:1podman tag mon-api:v3-multistage mon-api:latest -
Tag pour le registry
Fenêtre de terminal podman tag mon-api:1.0.0 registry.example.com/team/mon-api:1.0.0 -
Authentification
Fenêtre de terminal podman login registry.example.com -
Push
Fenêtre de terminal podman push registry.example.com/team/mon-api:1.0.0 -
Récupérer le digest
Fenêtre de terminal podman inspect registry.example.com/team/mon-api:1.0.0 \--format '{{.Digest}}'Résultat sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -
Documenter
Conservez le digest dans votre documentation de release :
Release 1.0.0Image: registry.example.com/team/mon-api:1.0.0Digest: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Tags vs Digests
Section intitulée « Tags vs Digests »| Tags | Digests | |
|---|---|---|
| Mutable | Oui (:latest peut changer) | Non (identifiant crypto) |
| Lisible | Oui (:1.0.0) | Non (sha256:abc...) |
| Traçable | Non | Oui (SBOM, signatures) |
Règle : utilisez les tags pour la lisibilité, conservez les digests pour la traçabilité.
Push vers différents registries
Section intitulée « Push vers différents registries »podman login docker.iopodman tag mon-api:1.0.0 docker.io/monuser/mon-api:1.0.0podman push docker.io/monuser/mon-api:1.0.0echo $GITHUB_TOKEN | podman login ghcr.io -u USERNAME --password-stdinpodman tag mon-api:1.0.0 ghcr.io/monuser/mon-api:1.0.0podman push ghcr.io/monuser/mon-api:1.0.0podman login registry.example.compodman tag mon-api:1.0.0 registry.example.com/team/mon-api:1.0.0podman push registry.example.com/team/mon-api:1.0.0Options avancées (cas particuliers)
Section intitulée « Options avancées (cas particuliers) »Ces options sont utiles dans des cas spécifiques. Ne les utilisez pas par défaut.
—squash : fusionner les layers
Section intitulée « —squash : fusionner les layers »--squash fusionne tous les layers en un seul.
| Avantage | Inconvénient |
|---|---|
| Image plus petite (un seul layer) | Perd l’historique des layers |
| Pas d’artefacts de build visibles | Cache inutilisable pour builds incrémentaux |
| Incompatible avec SBOM par layer |
# Squash toutpodman build --squash -t mon-image:squashed .
# Squash uniquement les layers ajoutés (garde le base layer)podman build --squash-all -t mon-image:all-squashed .Quand utiliser : si la traçabilité des layers n’est pas importante et que vous voulez une image minimale.
—layers : contrôler la création de layers
Section intitulée « —layers : contrôler la création de layers »# Créer un layer par instruction (défaut)podman build --layers -t mon-image:layered .
# Tout dans un seul layer (comme --squash mais différent internement)podman build --layers=false -t mon-image:single .—pull : contrôler le téléchargement de l’image de base
Section intitulée « —pull : contrôler le téléchargement de l’image de base »# Pull uniquement si l'image n'existe pas localement (défaut)podman build --pull=missing -t mon-image .
# Toujours pull (pour avoir la dernière version)podman build --pull=always -t mon-image .
# Jamais pull (utiliser le cache local)podman build --pull=never -t mon-image .Dépannage
Section intitulée « Dépannage »Problèmes courants
Section intitulée « Problèmes courants »| Symptôme | Cause | Solution |
|---|---|---|
COPY failed: file not found | Fichier hors contexte ou dans .containerignore | Vérifier le chemin et .containerignore |
| Build très lent | Contexte trop gros | Ajouter .containerignore |
| Cache jamais utilisé | Ordre des instructions | Mettre les deps avant le code |
| Image trop grosse | Pas de multi-stage | Utiliser multi-stage |
| Secret visible dans history | ARG/ENV pour secrets | Utiliser --secret |
| Push échoue | Pas authentifié | podman login |
Commandes de diagnostic
Section intitulée « Commandes de diagnostic »# Voir les logs détailléspodman build --log-level debug -t mon-image .
# Voir la taille de chaque layerpodman image history mon-image:1.0 --no-trunc
# Vérifier le contexte envoyétar -cvf - . 2>&1 | head -20
# Analyser le contenu de l'imagepodman run --rm -it mon-image:1.0 sh
# Vérifier qu'un secret n'est pas dans l'imagepodman history mon-image:1.0 --no-trunc | grep -i secretLe cache ne fonctionne pas
Section intitulée « Le cache ne fonctionne pas »Symptôme : chaque build refait tout.
Diagnostic :
# Voir quel layer invalide le cachepodman build -t test . 2>&1 | grep -E "(STEP|Using cache|COMMIT)"Causes fréquentes :
COPY . .trop tôt- Fichiers qui changent toujours dans le contexte (
.git/, logs) --no-cacheutilisé par erreur
Lab : construire une image de A à Z
Section intitulée « Lab : construire une image de A à Z »Objectif : construire une API FastAPI optimisée et la publier.
-
Créer le projet
Fenêtre de terminal mkdir -p ~/lab-build/app && cd ~/lab-buildapp/main.py from fastapi import FastAPIapp = FastAPI()@app.get("/")def read_root():return {"message": "Hello from optimized image!"}requirements.txt fastapi==0.115.0uvicorn[standard]==0.32.0 -
Build naïf (baseline)
Containerfile.naive FROM python:3.12-slimWORKDIR /appCOPY . .RUN pip install -r requirements.txtCMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]Fenêtre de terminal time podman build -f Containerfile.naive -t api:naive .podman images | grep apiNotez : taille et temps de build.
-
Build optimisé (multi-stage + cache)
Containerfile # BuilderFROM python:3.12.8-alpine3.21 AS builderWORKDIR /installCOPY requirements.txt .RUN pip install --no-cache-dir --prefix=/install -r requirements.txt# RuntimeFROM python:3.12.8-alpine3.21COPY --from=builder /install /usr/localWORKDIR /appCOPY app/ ./app/RUN adduser -D appuser && chown -R appuser /appUSER appuserEXPOSE 8000CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]Fenêtre de terminal time podman build -t api:optimized .podman images | grep api -
Mesurer le gain cache
Fenêtre de terminal # Modifier le codeecho "# comment" >> app/main.py# Rebuild (devrait être rapide)time podman build -t api:optimized .Le rebuild devrait prendre < 5 secondes (pip en cache).
-
Publier (optionnel)
Fenêtre de terminal # Tagpodman tag api:optimized registry.example.com/demo/api:1.0.0# Pushpodman login registry.example.compodman push registry.example.com/demo/api:1.0.0# Digestpodman inspect registry.example.com/demo/api:1.0.0 --format '{{.Digest}}'
Résultats attendus :
| Métrique | Naïf | Optimisé | Gain |
|---|---|---|---|
| Taille | ~300 MB | ~50 MB | -83% |
| Build initial | 45 sec | 30 sec | -33% |
| Rebuild (code change) | 45 sec | 3 sec | -93% |
À retenir
Section intitulée « À retenir »podman buildutilise Buildah en interne — stockage partagé, sans démon- Pinner les versions :
python:3.12.8-alpine3.21paspython:latest - Cache = ordre des instructions : dépendances avant code
.containerignore: exclure.git/,.venv/,node_modules/- Multi-stage : séparer builder (avec gcc) et runtime (sans)
- Secrets : utiliser
--secret, jamais ARG/ENV - Publier : tag + push + conserver le digest
- Squash : cas particulier, pas une best practice universelle