Registres et images — Distribution et sécurité#
Docker Hub : le registre public de référence#
Un registre est un serveur qui stocke et distribue des images Docker. Docker Hub (hub.docker.com) est le registre public par défaut — quand vous faites docker pull nginx, Docker va chercher l’image sur Docker Hub.
Catégories d’images sur Docker Hub#
Catégorie |
Exemple |
Caractéristique |
|---|---|---|
Images officielles |
|
Maintenues par Docker Inc. et les éditeurs, vérifiées, documentées |
Images vérifiées |
|
Publiées par des éditeurs de confiance certifiés par Docker |
Images communautaires |
|
Publiées par n’importe qui — à utiliser avec précaution |
Images d’organisation |
|
Namespace privé d’une entreprise |
Règle d’or pour les images de base
Ne faites jamais confiance à une image inconnue. Privilégiez toujours les images officielles ou les images vérifiées pour vos images de base. Une image malveillante peut contenir des backdoors ou des mineurs de cryptomonnaie.
Structure d’une référence d’image#
Une référence d’image Docker est plus complexe qu’elle n’y paraît. Voici sa structure complète :
registry/namespace/nom:tag@digest
│ │ │ │ └─ SHA256 immuable de l'image
│ │ │ └─ Version (latest, 3.12, alpine…)
│ │ └─ Nom de l'image (nginx, python…)
│ └─ Utilisateur ou organisation (monuser, bitnami…)
└─ Registre (docker.io par défaut, ghcr.io, gcr.io…)
Exemples concrets :
Référence |
Signification |
|---|---|
|
|
|
|
|
|
|
GitHub Container Registry, namespace org |
|
Image exacte identifiée par son digest SHA256 |
|
Google Container Registry |
Commandes de base pour les registres#
# S'authentifier sur Docker Hub
docker login
# S'authentifier sur un registre privé
docker login registry.mycompany.com
# Se déconnecter
docker logout registry.mycompany.com
# Télécharger une image
docker pull python:3.12-slim
# Télécharger avec son digest (immuable)
docker pull python@sha256:abc123def456...
# Tagger une image pour un registre
docker tag myapp:local registry.mycompany.com/backend/myapp:v2.1.0
# Pousser une image vers un registre
docker push registry.mycompany.com/backend/myapp:v2.1.0
# Rechercher sur Docker Hub
docker search nginx --filter is-official=true
# Lister les images locales
docker images
# Supprimer les images non utilisées
docker image prune
# Supprimer toutes les images inutilisées (y compris celles avec tag)
docker image prune -a
Registres privés : comparaison#
Quand votre entreprise ne peut pas utiliser Docker Hub (confidentialité, compliance, performance), elle déploie un registre privé :
fig, ax = plt.subplots(figsize=(13, 5.5))
ax.axis("off")
ax.set_facecolor("#f8f9fa")
fig.patch.set_facecolor("#f8f9fa")
ax.set_title("Comparaison des registres privés Docker", fontsize=13, fontweight="bold", pad=12)
registres = [
("Harbor\n(open-source)", ["Auto-hébergé", "UI web complète", "RBAC avancé", "Vulnerability scan", "Réplication", "Signatures (Notary)", "Proxy cache", "Gratuit"]),
("AWS ECR", ["Géré par AWS", "Intégré IAM", "Lifecycle policies", "Trivy scan natif", "Multi-région", "Immutable tags", "Pay-per-use", "Pas d'UI riche"]),
("GCP Artifact\nRegistry", ["Géré par GCP", "Multi-format", "VPC-SC support", "Binary auth", "Régional/global", "IAM intégré", "Scans Container Analysis", "Remplace GCR"]),
("Azure ACR", ["Géré par Azure", "Tâches ACR (CI)", "Geo-réplication", "Defender for Cloud", "Webhooks", "Token auth", "Intégration AKS", "Niveaux Basic/Standard/Premium"]),
]
colors = ["#d5f5e3", "#fde8d8", "#d6eaf8", "#fdf2f8"]
x_starts = [0.02, 0.26, 0.52, 0.76]
for (name, features), color, x in zip(registres, colors, x_starts):
box = FancyBboxPatch((x, 0.05), 0.22, 0.88, boxstyle="round,pad=0.01",
facecolor=color, edgecolor="#555", linewidth=1.5,
transform=ax.transAxes, clip_on=False)
ax.add_patch(box)
ax.text(x + 0.11, 0.88, name, ha="center", va="center", fontsize=10,
fontweight="bold", color="#2c3e50", transform=ax.transAxes)
for i, feat in enumerate(features):
y_pos = 0.78 - i * 0.092
ax.text(x + 0.02, y_pos, f"• {feat}", ha="left", va="center",
fontsize=8.5, color="#34495e", transform=ax.transAxes)
plt.tight_layout()
plt.show()
Distribution d’images : manifestes et layers#
Multi-arch : une image, plusieurs architectures#
Avec docker buildx, vous pouvez créer une image qui fonctionne sur différentes architectures (x86_64, ARM64, ARMv7…). Un manifest list (ou « OCI image index ») pointe vers plusieurs manifestes :
# Créer un builder multi-plateforme
docker buildx create --name multiarch --use
# Construire et pousser pour x86_64 et ARM64
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myregistry/myapp:v1.2.0 \
--push \
.
# Inspecter un manifest multi-arch
docker manifest inspect python:3.12-slim
fig, axes = plt.subplots(1, 2, figsize=(13, 5.5))
# --- Structure du manifest multi-arch ---
ax1 = axes[0]
ax1.set_xlim(0, 10)
ax1.set_ylim(0, 9)
ax1.axis("off")
ax1.set_facecolor("#f8f9fa")
ax1.set_title("Manifest list multi-architecture", fontsize=11, fontweight="bold", pad=8)
# Manifest list principal
ml_box = FancyBboxPatch((2.5, 7.0), 5.0, 1.5, boxstyle="round,pad=0.15",
facecolor="#d6eaf8", edgecolor="#2980b9", linewidth=2)
ax1.add_patch(ml_box)
ax1.text(5, 7.75, "Manifest List", ha="center", va="center", fontsize=11, fontweight="bold", color="#1a5276")
ax1.text(5, 7.2, "python:3.12-slim", ha="center", va="center", fontsize=9, color="#555",
bbox=dict(boxstyle="round,pad=0.2", facecolor="white", edgecolor="#aaa"))
# Manifestes par arch
arch_data = [
(1.0, 4.8, "linux/amd64", "sha256:a1b2c3…", "#d5f5e3"),
(4.5, 4.8, "linux/arm64", "sha256:d4e5f6…", "#fde8d8"),
(8.0, 4.8, "linux/arm/v7", "sha256:g7h8i9…", "#fdf2f8"),
]
for x, y, arch, digest, color in arch_data:
box = FancyBboxPatch((x - 1.3, y - 0.5), 2.6, 1.6, boxstyle="round,pad=0.1",
facecolor=color, edgecolor="#555", linewidth=1.5)
ax1.add_patch(box)
ax1.text(x, y + 0.65, arch, ha="center", va="center", fontsize=9.5, fontweight="bold", color="#2c3e50")
ax1.text(x, y + 0.1, digest, ha="center", va="center", fontsize=7.5, color="#555")
ax1.text(x, y - 0.2, "Manifest v2", ha="center", va="center", fontsize=7.5, color="#888")
ax1.annotate("", xy=(x, y + 1.1), xytext=(5, 7.0),
arrowprops=dict(arrowstyle="-|>", color="#555", lw=1.5))
# Layers sous chaque manifest
for x, y, _, _, color in arch_data:
for j, (size, name) in enumerate([(32, "OS base"), (2, "Python bins"), (5, "pip packages")]):
ly = y - 1.4 - j * 0.55
ly_box = FancyBboxPatch((x - 1.2, ly - 0.22), 2.4, 0.42, boxstyle="round,pad=0.05",
facecolor="white", edgecolor="#bbb", linewidth=1)
ax1.add_patch(ly_box)
ax1.text(x, ly, f"Layer: {name} ({size}MB)", ha="center", va="center", fontsize=7, color="#555")
# --- Déduplication des layers ---
ax2 = axes[1]
ax2.set_facecolor("#f8f9fa")
ax2.set_title("Déduplication des layers entre images", fontsize=11, fontweight="bold", pad=8)
images = ["myapp:v1.0", "myapp:v1.1", "myapp:v2.0"]
layers_content = [
["OS debian (120MB)", "Python 3.12 (45MB)", "deps v1 (23MB)", "app code v1 (2MB)"],
["OS debian (120MB)", "Python 3.12 (45MB)", "deps v1 (23MB)", "app code v2 (2.1MB)"],
["OS debian (120MB)", "Python 3.12 (45MB)", "deps v2 (28MB)", "app code v3 (2.3MB)"],
]
layer_colors = {
"OS debian (120MB)": "#aed6f1",
"Python 3.12 (45MB)": "#a9dfbf",
"deps v1 (23MB)": "#f9e79f",
"deps v2 (28MB)": "#f5cba7",
"app code v1 (2MB)": "#d7bde2",
"app code v2 (2.1MB)": "#c39bd3",
"app code v3 (2.3MB)": "#af7ac5",
}
x_positions = [1.5, 3.5, 5.5]
bar_width = 1.4
for xi, (img_name, layers) in zip(x_positions, zip(images, layers_content)):
y_bottom = 0
for layer in layers:
size_mb = float(re.search(r"([\d.]+)MB", layer).group(1))
color = layer_colors[layer]
ax2.bar(xi, size_mb, bottom=y_bottom, width=bar_width, color=color,
edgecolor="white", linewidth=1.5)
if size_mb > 10:
ax2.text(xi, y_bottom + size_mb / 2, layer.split("(")[0].strip(),
ha="center", va="center", fontsize=7.5, color="#2c3e50")
y_bottom += size_mb
ax2.text(xi, -8, img_name, ha="center", va="top", fontsize=9.5, fontweight="bold", color="#2c3e50")
# Annotation déduplication
ax2.annotate("Partagé !\n(téléchargé 1×)", xy=(3.5, 90), xytext=(6.5, 110),
arrowprops=dict(arrowstyle="->", color="#e74c3c", lw=1.5),
fontsize=9, color="#e74c3c", fontweight="bold")
ax2.set_ylabel("Taille (MB)", fontsize=10)
ax2.set_xticks([])
ax2.set_title("Déduplication des layers entre images", fontsize=11, fontweight="bold", pad=8)
# Légende des layers partagés
total_no_dedup = sum(sum(float(re.search(r"([\d.]+)MB", l).group(1)) for l in layers)
for layers in layers_content)
unique_layers = set(l for layers in layers_content for l in layers)
total_dedup = sum(float(re.search(r"([\d.]+)MB", l).group(1)) for l in unique_layers)
ax2.text(0.97, 0.97, f"Sans dédup : {total_no_dedup:.0f} MB\nAvec dédup : {total_dedup:.0f} MB\nEconomie : {100*(1-total_dedup/total_no_dedup):.0f}%",
ha="right", va="top", transform=ax2.transAxes, fontsize=9,
bbox=dict(boxstyle="round,pad=0.4", facecolor="#eafaf1", edgecolor="#27ae60"))
plt.tight_layout()
plt.show()
Vulnerability scanning : analyser la sécurité des images#
Un scanner de vulnérabilités analyse les packages installés dans une image et les compare aux bases de données CVE (Common Vulnerabilities and Exposures).
# Trivy — scanner open-source très populaire
trivy image python:3.12-slim
# Docker Scout — intégré dans Docker Desktop
docker scout cves python:3.12-slim
# Scan dans un pipeline CI (GitHub Actions)
# - uses: aquasecurity/trivy-action@master
# with:
# image-ref: 'myapp:${{ github.sha }}'
# severity: 'CRITICAL,HIGH'
# exit-code: '1' # Fail le pipeline si vulnérabilités critiques
# Simulation d'un rapport de vulnérabilités
vuln_data = {
"image": "python:3.11-slim",
"digest": "sha256:b5b2b2c5…",
"scan_date": "2026-03-21",
"vulnerabilities": [
{"id": "CVE-2023-4563", "pkg": "libssl1.1", "severity": "CRITICAL", "fixed_in": "1.1.1w-0+deb11u2"},
{"id": "CVE-2024-0727", "pkg": "openssl", "severity": "HIGH", "fixed_in": "3.0.12"},
{"id": "CVE-2023-5752", "pkg": "pip", "severity": "HIGH", "fixed_in": "23.3"},
{"id": "CVE-2024-3094", "pkg": "xz-utils", "severity": "CRITICAL", "fixed_in": "5.6.2"},
{"id": "CVE-2023-6246", "pkg": "libc6", "severity": "HIGH", "fixed_in": "2.36-9+deb12u4"},
{"id": "CVE-2024-0646", "pkg": "linux-libc-dev", "severity": "MEDIUM", "fixed_in": "6.1.76-1"},
{"id": "CVE-2023-5678", "pkg": "libssl1.1", "severity": "MEDIUM", "fixed_in": "1.1.1w-0+deb11u2"},
{"id": "CVE-2024-2236", "pkg": "libgcrypt20", "severity": "MEDIUM", "fixed_in": "1.10.3-2"},
{"id": "CVE-2023-7104", "pkg": "libsqlite3-0", "severity": "LOW", "fixed_in": "3.40.1-2"},
{"id": "CVE-2023-4016", "pkg": "procps", "severity": "LOW", "fixed_in": "2:4.0.2-3"},
]
}
# Affichage du rapport
severity_colors = {"CRITICAL": "#c0392b", "HIGH": "#e67e22", "MEDIUM": "#f1c40f", "LOW": "#27ae60"}
severity_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
counts = defaultdict(int)
for v in vuln_data["vulnerabilities"]:
counts[v["severity"]] += 1
print(f"Rapport de vulnérabilités — {vuln_data['image']}")
print(f"Scané le : {vuln_data['scan_date']}")
print(f"Digest : {vuln_data['digest']}")
print()
print(f"{'Sévérité':<12} {'Nombre':>8} {'Barre'}")
print("-" * 45)
for sev in severity_order:
n = counts.get(sev, 0)
bar = "█" * n
print(f" {sev:<10} {n:>6} {bar}")
print()
print(f"{'CVE':<20} {'Package':<20} {'Sévérité':<10} {'Corrigé dans'}")
print("-" * 70)
for v in sorted(vuln_data["vulnerabilities"],
key=lambda x: severity_order.index(x["severity"])):
print(f" {v['id']:<18} {v['pkg']:<20} {v['severity']:<10} {v['fixed_in']}")
Rapport de vulnérabilités — python:3.11-slim
Scané le : 2026-03-21
Digest : sha256:b5b2b2c5…
Sévérité Nombre Barre
---------------------------------------------
CRITICAL 2 ██
HIGH 3 ███
MEDIUM 3 ███
LOW 2 ██
CVE Package Sévérité Corrigé dans
----------------------------------------------------------------------
CVE-2023-4563 libssl1.1 CRITICAL 1.1.1w-0+deb11u2
CVE-2024-3094 xz-utils CRITICAL 5.6.2
CVE-2024-0727 openssl HIGH 3.0.12
CVE-2023-5752 pip HIGH 23.3
CVE-2023-6246 libc6 HIGH 2.36-9+deb12u4
CVE-2024-0646 linux-libc-dev MEDIUM 6.1.76-1
CVE-2023-5678 libssl1.1 MEDIUM 1.1.1w-0+deb11u2
CVE-2024-2236 libgcrypt20 MEDIUM 1.10.3-2
CVE-2023-7104 libsqlite3-0 LOW 3.40.1-2
CVE-2023-4016 procps LOW 2:4.0.2-3
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# --- Camembert des vulnérabilités ---
ax1 = axes[0]
labels = [s for s in severity_order if counts.get(s, 0) > 0]
sizes = [counts[s] for s in labels]
colors_pie = ["#c0392b", "#e67e22", "#f1c40f", "#27ae60"][:len(labels)]
explode = [0.05 if s in ("CRITICAL", "HIGH") else 0 for s in labels]
wedges, texts, autotexts = ax1.pie(
sizes, labels=labels, colors=colors_pie, explode=explode,
autopct="%1.0f%%", startangle=90, textprops={"fontsize": 11}
)
for autotext in autotexts:
autotext.set_fontsize(10)
autotext.set_fontweight("bold")
ax1.set_title(f"Vulnérabilités par sévérité\n{vuln_data['image']}", fontsize=11, fontweight="bold")
# Annotation centre
total = sum(sizes)
ax1.text(0, 0, f"{total}\ntotal", ha="center", va="center", fontsize=12, fontweight="bold", color="#2c3e50")
# --- Score de sécurité comparatif ---
ax2 = axes[1]
images_compare = [
("python:3.9", 3, 8, 12, 5),
("python:3.11-slim", 2, 4, 4, 2),
("python:3.12-slim", 0, 1, 3, 1),
("python:3.12-alpine", 0, 0, 1, 0),
]
severities = ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
sev_colors_bar = ["#c0392b", "#e67e22", "#f1c40f", "#a9dfbf"]
img_names = [img[0] for img in images_compare]
x = np.arange(len(img_names))
width = 0.2
for i, (sev, col) in enumerate(zip(severities, sev_colors_bar)):
vals = [img[i + 1] for img in images_compare]
bars = ax2.bar(x + i * width, vals, width, label=sev, color=col, edgecolor="white", lw=1.2)
ax2.set_xticks(x + width * 1.5)
ax2.set_xticklabels(img_names, rotation=20, ha="right", fontsize=9.5)
ax2.set_ylabel("Nombre de vulnérabilités", fontsize=10)
ax2.set_title("Comparaison de la surface d'attaque\nselon l'image de base", fontsize=11, fontweight="bold")
ax2.legend(title="Sévérité", fontsize=9)
ax2.set_ylim(0, 18)
ax2.annotate("Alpine = surface\nd'attaque minimale", xy=(3 + width*1.5, 1.5),
xytext=(2.5, 12),
arrowprops=dict(arrowstyle="->", color="#27ae60", lw=1.5),
fontsize=9, color="#27ae60", fontweight="bold")
plt.tight_layout()
plt.show()
Signatures d’images et chaîne de confiance#
Cosign et Sigstore#
Cosign (projet Sigstore) permet de signer cryptographiquement des images. Cela garantit que l’image que vous déployez est bien celle qui a été construite par votre pipeline CI, et qu’elle n’a pas été altérée.
# Générer une paire de clés
cosign generate-key-pair
# Signer une image après le push
cosign sign --key cosign.key myregistry/myapp:v1.2.0
# Vérifier la signature avant le déploiement
cosign verify --key cosign.pub myregistry/myapp:v1.2.0
# Signer avec une identité OIDC (keyless — GitHub Actions, etc.)
cosign sign myregistry/myapp:v1.2.0 # Utilise l'identité OIDC du runner CI
# Attacher un SBOM (Software Bill of Materials)
cosign attach sbom --sbom sbom.json myregistry/myapp:v1.2.0
SBOM : l’inventaire de votre image
Un SBOM (Software Bill of Materials) est un inventaire exhaustif de tous les composants d’une image : packages système, bibliothèques Python, licences… C’est devenu une exigence légale dans certains secteurs (gouvernement américain, infrastructures critiques). Outils : syft, trivy sbom, docker sbom.
Code Python : simulation d’un registre minimaliste#
import hashlib
import json
from datetime import datetime, timezone
from typing import Optional
class Layer:
"""Représente une couche (layer) d'image."""
def __init__(self, content: bytes, media_type: str = "application/vnd.oci.image.layer.v1.tar+gzip"):
self.content = content
self.media_type = media_type
self.digest = self._compute_digest()
self.size = len(content)
def _compute_digest(self) -> str:
return "sha256:" + hashlib.sha256(self.content).hexdigest()
def __repr__(self):
return f"Layer(digest={self.digest[:19]}…, size={self.size}B)"
class ImageManifest:
"""Manifest d'une image (simplifié)."""
def __init__(self, config: dict, layers: list[Layer]):
self.config = config
self.layers = layers
self.created_at = datetime.now(timezone.utc).isoformat()
self._raw = self._build()
self.digest = "sha256:" + hashlib.sha256(json.dumps(self._raw).encode()).hexdigest()
def _build(self) -> dict:
return {
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:" + hashlib.sha256(json.dumps(self.config).encode()).hexdigest(),
"size": len(json.dumps(self.config).encode()),
},
"layers": [
{"mediaType": l.media_type, "digest": l.digest, "size": l.size}
for l in self.layers
],
}
def to_dict(self) -> dict:
return self._raw
class Registry:
"""Registre d'images minimaliste (en mémoire)."""
def __init__(self, name: str):
self.name = name
self._blobs: dict[str, bytes] = {} # digest → contenu binaire
self._manifests: dict[str, ImageManifest] = {} # digest → manifest
self._tags: dict[str, dict[str, str]] = {} # namespace/image → {tag: digest}
self._pull_count: dict[str, int] = defaultdict(int)
def push(self, namespace: str, name: str, tag: str, manifest: ImageManifest) -> str:
"""Pousse une image (manifest + layers) dans le registre."""
# Stocker les layers (déduplication automatique par digest)
for layer in manifest.layers:
if layer.digest not in self._blobs:
self._blobs[layer.digest] = layer.content
print(f" → Nouveau layer stocké : {layer.digest[:23]}… ({layer.size}B)")
else:
print(f" → Layer déjà présent : {layer.digest[:23]}… (SKIP)")
# Stocker le manifest
self._manifests[manifest.digest] = manifest
# Mettre à jour le tag
repo = f"{namespace}/{name}"
if repo not in self._tags:
self._tags[repo] = {}
self._tags[repo][tag] = manifest.digest
ref = f"{self.name}/{repo}:{tag}"
print(f" ✓ Image poussée : {ref}")
print(f" Digest : {manifest.digest[:30]}…")
return manifest.digest
def pull(self, namespace: str, name: str, tag: str) -> Optional[ImageManifest]:
"""Télécharge un manifest depuis le registre."""
repo = f"{namespace}/{name}"
if repo not in self._tags or tag not in self._tags[repo]:
print(f" ✗ Image introuvable : {self.name}/{repo}:{tag}")
return None
digest = self._tags[repo][tag]
self._pull_count[f"{repo}:{tag}"] += 1
return self._manifests[digest]
def tag(self, namespace: str, name: str, src_tag: str, dst_tag: str) -> bool:
"""Ajoute un tag à une image existante."""
repo = f"{namespace}/{name}"
if repo not in self._tags or src_tag not in self._tags[repo]:
return False
self._tags[repo][dst_tag] = self._tags[repo][src_tag]
print(f" ✓ Tag ajouté : {repo}:{src_tag} → {repo}:{dst_tag}")
return True
def list_tags(self, namespace: str, name: str) -> list[str]:
repo = f"{namespace}/{name}"
return list(self._tags.get(repo, {}).keys())
def stats(self):
total_blob_size = sum(len(v) for v in self._blobs.values())
print(f"\nRegistre : {self.name}")
print(f" Blobs (layers uniques) : {len(self._blobs)} — {total_blob_size}B")
print(f" Manifests : {len(self._manifests)}")
print(f" Repositories : {len(self._tags)}")
for repo, tags in self._tags.items():
print(f" {repo}")
for tag, digest in tags.items():
pulls = self._pull_count.get(f"{repo}:{tag}", 0)
print(f" :{tag:<15} {digest[:26]}… (pulls: {pulls})")
# === Démonstration ===
print("=" * 60)
print("Simulation d'un registre Docker minimaliste")
print("=" * 60)
registry = Registry("registry.example.com")
# Layer partagé entre v1 et v2 (OS de base)
layer_os = Layer(b"debian-bookworm-slim-base-layer-content-x86_64" * 100)
layer_python = Layer(b"python-3.12-binaries-and-stdlib-content" * 50)
layer_deps_v1 = Layer(b"pip-dependencies-v1-requirements-txt-hash-abc" * 20)
layer_app_v1 = Layer(b"myapp-source-code-version-1.0.0-compiled")
layer_deps_v2 = Layer(b"pip-dependencies-v2-requirements-txt-hash-def" * 20)
layer_app_v2 = Layer(b"myapp-source-code-version-2.0.0-compiled-with-new-feature")
config_v1 = {"author": "CI/CD", "architecture": "amd64", "os": "linux",
"config": {"Cmd": ["python", "app.py"], "ExposedPorts": {"8000/tcp": {}}}}
config_v2 = {**config_v1, "config": {**config_v1["config"], "Labels": {"version": "2.0.0"}}}
manifest_v1 = ImageManifest(config_v1, [layer_os, layer_python, layer_deps_v1, layer_app_v1])
manifest_v2 = ImageManifest(config_v2, [layer_os, layer_python, layer_deps_v2, layer_app_v2])
print("\n--- Push myapp:v1.0.0 ---")
registry.push("myorg", "myapp", "v1.0.0", manifest_v1)
print("\n--- Push myapp:v2.0.0 (layer OS et Python déjà présents) ---")
registry.push("myorg", "myapp", "v2.0.0", manifest_v2)
print("\n--- Tag latest → v2.0.0 ---")
registry.tag("myorg", "myapp", "v2.0.0", "latest")
print("\n--- Pull myapp:latest ---")
m = registry.pull("myorg", "myapp", "latest")
if m:
print(f" Manifest reçu : {len(m.layers)} layers")
print("\n--- Pull myapp:latest (2ème fois) ---")
registry.pull("myorg", "myapp", "latest")
registry.stats()
============================================================
Simulation d'un registre Docker minimaliste
============================================================
--- Push myapp:v1.0.0 ---
→ Nouveau layer stocké : sha256:df5b199c0e8fc3f8… (4600B)
→ Nouveau layer stocké : sha256:6b9ec5f561ebb4ca… (1950B)
→ Nouveau layer stocké : sha256:da328dea50c2d583… (900B)
→ Nouveau layer stocké : sha256:00223c90ce4bb87f… (40B)
✓ Image poussée : registry.example.com/myorg/myapp:v1.0.0
Digest : sha256:55d937cd5cebe052d8642f7…
--- Push myapp:v2.0.0 (layer OS et Python déjà présents) ---
→ Layer déjà présent : sha256:df5b199c0e8fc3f8… (SKIP)
→ Layer déjà présent : sha256:6b9ec5f561ebb4ca… (SKIP)
→ Nouveau layer stocké : sha256:cd4e9e8a11c8a4d8… (900B)
→ Nouveau layer stocké : sha256:6bd6b99a502eeb98… (57B)
✓ Image poussée : registry.example.com/myorg/myapp:v2.0.0
Digest : sha256:87ef1221de4d33188fd2613…
--- Tag latest → v2.0.0 ---
✓ Tag ajouté : myorg/myapp:v2.0.0 → myorg/myapp:latest
--- Pull myapp:latest ---
Manifest reçu : 4 layers
--- Pull myapp:latest (2ème fois) ---
Registre : registry.example.com
Blobs (layers uniques) : 6 — 8447B
Manifests : 2
Repositories : 1
myorg/myapp
:v1.0.0 sha256:55d937cd5cebe052d86… (pulls: 0)
:v2.0.0 sha256:87ef1221de4d33188fd… (pulls: 0)
:latest sha256:87ef1221de4d33188fd… (pulls: 2)
Nettoyage et politique de rétention#
# Supprimer les images non taggées localement
docker image prune
# Supprimer TOUTES les images inutilisées (y compris avec tag)
docker image prune -a
# Supprimer une image précise
docker rmi myapp:v1.0.0
# Nettoyer tout (images + conteneurs arrêtés + réseaux + cache build)
docker system prune -a
# Voir l'utilisation disque
docker system df
docker system df -v # Détail par image
Dans Harbor ou les registres cloud, vous pouvez définir des politiques de rétention :
# Exemple de politique de rétention Harbor (résumé)
# Garder les 10 derniers tags triés par date de push
# pour les repos matchant myorg/**
rules:
- repositories: matching "**"
retain: 10
by: lastPushed
exclude_tags: ["latest", "stable"]
Points clés à retenir#
Résumé du chapitre
Docker Hub est le registre public — préférez les images officielles et vérifiées
Une référence d’image complète :
registry/namespace/nom:tag@digestLes layers sont dédupliqués — deux images partageant les mêmes couches ne les stockent qu’une fois
Ne jamais utiliser
latesten production — utilisez des tags précis ou des digestsMulti-arch :
docker buildxpermet de construire une image pour x86_64 et ARM64 simultanémentVulnerability scanning (Trivy, Docker Scout) : à intégrer dans votre CI/CD
Cosign/Sigstore : signer vos images pour garantir leur intégrité
Images Alpine : surface d’attaque minimale — à privilégier pour les images de production
Comment une image est-elle stockée et distribuée ?#
Une image Docker n’est pas un seul fichier monolithique. Elle est composée de layers (couches), et un manifest décrit l’ensemble :
Manifest Image Manifest v2 : { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 7023, "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 31366757, "digest": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 1073, "digest": "sha256:9b9b7f3d56a01e3d9076874990c62e7a516cc4032f784f421574d06b18ef9aa4" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 11691195, "digest": "sha256:14ca88e9f6723ce82bc14b96ccfc5123742032c1f8a2b7ab5a11ea3894b5b308" } ] } Nombre de layers : 3 Taille totale des layers : 41.1 MB Taille de la config : 7023 bytes Principe de déduplication : Si deux images partagent un layer (même digest SHA256), Docker ne le télécharge et ne le stocke QU'UNE SEULE FOIS.