Aller au contenu
Conteneurs & Orchestration medium

Distribution : déployer une registry privée on-premise (ex Docker Registry)

21 min de lecture

Vous voulez héberger vos images Docker en interne sans dépendre de Docker Hub ou Quay.io ? Distribution (anciennement Docker Registry v2) est le projet CNCF officiel pour créer une registry privée on-premise. Léger, stateless et compatible OCI, il se déploie en quelques minutes avec Docker Compose.

Ce guide couvre le déploiement complet d'une registry privée, de l'installation basique jusqu'à la configuration production avec TLS et authentification.

  • Déployer une registry avec Docker ou Docker Compose
  • Configurer TLS avec Let's Encrypt ou certificats auto-signés
  • Ajouter l'authentification htpasswd ou token
  • Configurer le stockage : filesystem, S3, Azure, GCS
  • Exécuter le garbage collection pour libérer l'espace disque
  • Intégrer avec Kubernetes et vos pipelines CI/CD

Distribution est le projet open source officiel de la CNCF (Cloud Native Computing Foundation) pour implémenter une registry de conteneurs. C'est le même code que celui utilisé par Docker Hub, GitHub Container Registry et de nombreuses registries cloud.

Le projet a connu plusieurs noms au fil du temps, ce qui peut prêter à confusion :

PériodeNomNotes
2013-2015Docker Registry v1Format propriétaire, abandonné
2015-2020Docker Registry v2Nouveau format, base du standard OCI
2020-présentCNCF DistributionProjet transféré à la CNCF, image registry:2
2024-présentDistribution 3.0Version majeure avec support OCI natif amélioré

Quand utiliser Distribution plutôt qu'une solution plus complète comme Harbor ?

CritèreDistributionHarborQuay (self-hosted)
ComplexitéTrès simpleMoyenneÉlevée
Ressources~50 MB RAM~2 GB RAM~4 GB RAM
Interface web❌ Non✅ Oui✅ Oui
Scan de sécurité❌ Non✅ Trivy/Clair✅ Clair
RBAC avancé❌ Non✅ Oui✅ Oui
Réplication❌ Non✅ Oui✅ Oui
Cas d'usageDev, CI/CD, petit clusterEntrepriseEntreprise Red Hat

En résumé : Distribution est idéal pour les environnements où vous avez besoin d'une registry simple, rapide à déployer, sans interface graphique ni fonctionnalités avancées. Pour la production enterprise, préférez Harbor.

Commençons par le déploiement le plus simple : une registry locale sans authentification ni TLS. Parfait pour le développement ou les tests.

Le volume nommé registry-data est le seul élément à ne pas oublier : sans lui, toutes les images poussées disparaissent à la suppression du conteneur. Le port 5000 est celui écouté par défaut par l'image registry:2.

Fenêtre de terminal
# Lancer une registry sur le port 5000
docker run -d \
--name registry \
--restart always \
-p 5000:5000 \
-v registry-data:/var/lib/registry \
registry:2
# Vérifier que la registry fonctionne
curl http://localhost:5000/v2/
# Résultat attendu : {}

Le nom de la registry fait partie du tag de l'image : c'est localhost:5000/ en préfixe qui indique à Docker où pousser. La séquence ci-dessous vérifie le cycle complet, de l'envoi jusqu'au retéléchargement après suppression locale.

Fenêtre de terminal
# Taguer une image existante pour la registry locale
docker pull alpine:3.19
docker tag alpine:3.19 localhost:5000/alpine:3.19
# Pousser vers la registry
docker push localhost:5000/alpine:3.19
# Vérifier la présence
curl http://localhost:5000/v2/_catalog
# {"repositories":["alpine"]}
# Supprimer l'image locale et la retirer
docker rmi localhost:5000/alpine:3.19
docker pull localhost:5000/alpine:3.19

Pour un déploiement plus robuste avec TLS et authentification, utilisez Docker Compose avec un fichier de configuration dédié.

Trois éléments cohabitent à côté du fichier Compose : la configuration de la registry, le fichier de mots de passe et les certificats TLS. Les monter en lecture seule dans le conteneur évite qu'un processus de la registry ne les modifie.

registry/
├── docker-compose.yml
├── config.yml
├── auth/
│ └── htpasswd
└── certs/
├── domain.crt
└── domain.key

Créez config.yml avec les paramètres de votre registry :

version: 0.1
log:
level: info
formatter: text
fields:
service: registry
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
cache:
blobdescriptor: inmemory
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
tls:
certificate: /certs/domain.crt
key: /certs/domain.key
auth:
htpasswd:
realm: Registry Realm
path: /auth/htpasswd
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

Créez docker-compose.yml. Le fichier config.yml est monté à l'emplacement attendu par l'image, /etc/docker/registry/config.yml, et remplace donc la configuration embarquée. La variable REGISTRY_STORAGE_DELETE_ENABLED est l'équivalent en variable d'environnement du bloc delete du fichier : Distribution accepte les deux, la variable ayant priorité.

services:
registry:
image: registry:2.8@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373
container_name: registry
restart: always
ports:
- "5000:5000"
environment:
REGISTRY_STORAGE_DELETE_ENABLED: "true"
volumes:
- ./config.yml:/etc/docker/registry/config.yml:ro
- ./auth:/auth:ro
- ./certs:/certs:ro
- registry-data:/var/lib/registry
volumes:
registry-data:

Une registry de production doit être protégée par TLS. Docker refuse par défaut de communiquer avec une registry HTTP non sécurisée (sauf localhost).

La solution la plus simple pour obtenir un certificat valide automatiquement. Traefik gère le renouvellement et le reverse proxy.

services:
traefik:
image: traefik:v3.0@sha256:a208c74fd80a566d4ea376053bff73d31616d7af3f1465a7747b8b89ee34d97e
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=admin@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- letsencrypt:/letsencrypt
registry:
image: registry:2.8@sha256:a3d8aaa63ed8681a604f1dea0aa03f100d5895b6a58ace528858a7b332415373
labels:
- "traefik.enable=true"
- "traefik.http.routers.registry.rule=Host(`registry.example.com`)"
- "traefik.http.routers.registry.entrypoints=websecure"
- "traefik.http.routers.registry.tls.certresolver=letsencrypt"
volumes:
- registry-data:/var/lib/registry
volumes:
registry-data:
letsencrypt:

Pour les environnements internes sans accès Internet, générez un certificat auto-signé. Les clients devront faire confiance à ce certificat.

  1. Générer le certificat

    Fenêtre de terminal
    # Créer le répertoire des certificats
    mkdir -p certs
    # Générer une clé privée et un certificat (valide 365 jours)
    openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
    -keyout certs/domain.key \
    -out certs/domain.crt \
    -subj "/CN=registry.local" \
    -addext "subjectAltName=DNS:registry.local,DNS:localhost,IP:192.168.1.100"
  2. Configurer Docker pour faire confiance au certificat

    Sur chaque machine cliente :

    Fenêtre de terminal
    # Créer le répertoire de certificats pour la registry
    sudo mkdir -p /etc/docker/certs.d/registry.local:5000
    # Copier le certificat
    sudo cp certs/domain.crt /etc/docker/certs.d/registry.local:5000/ca.crt
    # Redémarrer Docker
    sudo systemctl restart docker
  3. Tester la connexion

    Fenêtre de terminal
    docker login registry.local:5000
    # Username: admin
    # Password: ********
    # Login Succeeded

Distribution supporte plusieurs méthodes d'authentification. La plus simple est htpasswd, suffisante pour la plupart des cas d'usage.

L'authentification htpasswd utilise un fichier de mots de passe hashés, comme Apache. Simple à mettre en place, mais sans gestion fine des permissions (tout ou rien).

  1. Créer le fichier htpasswd

    Fenêtre de terminal
    # Créer le répertoire auth
    mkdir -p auth
    # Créer le premier utilisateur (bcrypt)
    docker run --rm --entrypoint htpasswd \
    httpd:2-alpine -Bbn admin motdepasse_securise > auth/htpasswd
    # Ajouter d'autres utilisateurs
    docker run --rm --entrypoint htpasswd \
    httpd:2-alpine -Bbn ci_user autre_motdepasse >> auth/htpasswd
  2. Configurer la registry

    Dans config.yml :

    auth:
    htpasswd:
    realm: Registry Realm
    path: /auth/htpasswd
  3. Tester l'authentification

    Fenêtre de terminal
    # Sans auth : erreur 401
    curl https://registry.local:5000/v2/
    # {"errors":[{"code":"UNAUTHORIZED",...}]}
    # Avec auth : succès
    curl -u admin:motdepasse_securise https://registry.local:5000/v2/
    # {}

Pour une gestion plus fine des permissions (lecture seule, écriture, par repository), utilisez un serveur d'authentification externe compatible avec le protocole Docker Registry Token.

Des projets comme Cesanta Docker Auth ou Portus implémentent ce protocole et permettent de définir des ACL granulaires.

Par défaut, Distribution stocke les images sur le filesystem local. Pour la production, vous pouvez utiliser un stockage objet (S3, GCS, Azure Blob).

Configuration simple pour un serveur unique. Le stockage est dans un volume Docker ou un répertoire local.

storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true

Pour la haute disponibilité et la scalabilité, utilisez un stockage objet S3. Compatible avec MinIO, Ceph, Wasabi et tout service S3-compatible.

storage:
s3:
accesskey: AKIAIOSFODNN7EXAMPLE
secretkey: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region: eu-west-1
bucket: my-registry-bucket
rootdirectory: /registry
encrypt: true
secure: true
v4auth: true
delete:
enabled: true
cache:
blobdescriptor: inmemory

Sur Azure, la registry écrit dans un conteneur de stockage Blob, qui doit exister avant le démarrage. Comme pour S3, la clé de compte ne doit pas figurer en clair dans le fichier : passez-la par variable d'environnement.

storage:
azure:
accountname: myaccount
accountkey: ${AZURE_STORAGE_KEY}
container: registry
realm: core.windows.net

Sur GCS, l'authentification passe par un fichier de clé de compte de service monté dans le conteneur. Le chemin indiqué par keyfile est lu au démarrage : s'il est absent, la registry refuse de démarrer.

storage:
gcs:
bucket: my-registry-bucket
keyfile: /path/to/keyfile.json
rootdirectory: /registry

Les images Docker sont composées de layers (blobs) partagés entre images. Quand vous supprimez un tag, les blobs ne sont pas automatiquement supprimés, ils peuvent encore être référencés par d'autres images.

Le garbage collection (GC) identifie et supprime les blobs orphelins pour récupérer l'espace disque.

Avant le GC, vous devez supprimer les tags que vous ne voulez plus conserver.

Fenêtre de terminal
# Lister les tags d'une image
curl -u admin:password https://registry.local:5000/v2/myapp/tags/list
# Récupérer le digest d'un tag
DIGEST=$(curl -s -u admin:password \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://registry.local:5000/v2/myapp/manifests/v1.0.0 \
-I | grep -i docker-content-digest | awk '{print $2}' | tr -d '\r')
# Supprimer le manifest (soft delete)
curl -u admin:password -X DELETE \
https://registry.local:5000/v2/myapp/manifests/$DIGEST

Le GC doit être exécuté registry arrêtée ou en mode read-only pour éviter les corruptions.

Fenêtre de terminal
# Arrêter la registry
docker stop registry
# Dry-run : voir ce qui serait supprimé
docker run --rm \
-v registry-data:/var/lib/registry \
-v $(pwd)/config.yml:/etc/docker/registry/config.yml \
registry:2 garbage-collect --dry-run /etc/docker/registry/config.yml
# Exécution réelle
docker run --rm \
-v registry-data:/var/lib/registry \
-v $(pwd)/config.yml:/etc/docker/registry/config.yml \
registry:2 garbage-collect /etc/docker/registry/config.yml
# Redémarrer la registry
docker start registry

Pour utiliser votre registry privée avec Kubernetes, créez un Secret de type docker-registry et référencez-le dans vos Pods.

Le Secret contient les identifiants que kubelet utilisera pour télécharger les images. Il est lié à un namespace : créez-le dans chaque namespace où des Pods tirent des images de votre registry, l'exemple ci-dessous cible default.

Fenêtre de terminal
kubectl create secret docker-registry registry-credentials \
--docker-server=registry.local:5000 \
--docker-username=admin \
--docker-password=motdepasse_securise \
--docker-email=admin@example.com \
-n default

Le champ imagePullSecrets référence le Secret par son nom. Sans lui, le Pod reste bloqué en ImagePullBackOff avec une erreur d'authentification, même si le Secret existe dans le namespace.

apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: registry.local:5000/myapp:v1.0.0
imagePullSecrets:
- name: registry-credentials

Si vous utilisez un certificat auto-signé, configurez containerd sur chaque nœud Kubernetes pour faire confiance à votre registry.

Fenêtre de terminal
# Créer le répertoire de configuration
sudo mkdir -p /etc/containerd/certs.d/registry.local:5000
# Créer hosts.toml
cat << 'EOF' | sudo tee /etc/containerd/certs.d/registry.local:5000/hosts.toml
server = "https://registry.local:5000"
[host."https://registry.local:5000"]
capabilities = ["pull", "resolve", "push"]
ca = "/etc/containerd/certs.d/registry.local:5000/ca.crt"
EOF
# Copier le certificat CA
sudo cp domain.crt /etc/containerd/certs.d/registry.local:5000/ca.crt
# Redémarrer containerd
sudo systemctl restart containerd

Voici un workflow pour pousser vos images vers votre registry privée depuis GitHub Actions.

Créez des secrets pour l'authentification à votre registry privée :

SecretValeur
REGISTRY_URLregistry.example.com:5000
REGISTRY_USERNAMEci_user
REGISTRY_PASSWORDMot de passe htpasswd

Trois réglages de sécurité structurent ce workflow. permissions: {} au niveau du fichier retire tous les droits du jeton GITHUB_TOKEN, chaque job redemandant ensuite le strict nécessaire (ici contents: read pour cloner). persist-credentials: false empêche actions/checkout de laisser le jeton dans la configuration Git du runner, où n'importe quelle étape suivante pourrait le lire. Enfin, chaque action est épinglée par SHA avec son numéro de version en commentaire.

name: Build and Push to Private Registry
on:
push:
branches: [main]
tags: ['v*']
permissions: {}
env:
IMAGE_NAME: myapp
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Login to Private Registry
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ secrets.REGISTRY_URL }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix=
- name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

Distribution expose une API REST standard (OCI Distribution Spec) pour interagir programmatiquement avec la registry.

Ces routes sont celles de la spécification OCI Distribution : elles fonctionnent à l'identique sur Docker Hub, Harbor ou GitHub Container Registry. Les segments entre accolades sont des variables, {name} étant le nom du repository et {ref} un tag ou un digest.

EndpointMéthodeDescription
/v2/GETVérifier que la registry fonctionne
/v2/_catalogGETLister tous les repositories
/v2/{name}/tags/listGETLister les tags d'une image
/v2/{name}/manifests/{ref}GETRécupérer un manifest
/v2/{name}/manifests/{ref}DELETESupprimer un manifest
/v2/{name}/blobs/{digest}GETTélécharger un blob

Ces requêtes servent à inventorier le contenu de la registry depuis un script, sans passer par le client Docker. L'en-tête Accept de la dernière commande n'est pas facultatif : il indique à la registry quel format de manifest vous acceptez. Sans lui, elle peut répondre dans un autre format, dont le digest diffère de celui que manipule docker pull.

Fenêtre de terminal
# Lister tous les repositories
curl -u admin:password https://registry.local:5000/v2/_catalog
# {"repositories":["alpine","myapp","nginx"]}
# Lister les tags d'une image
curl -u admin:password https://registry.local:5000/v2/myapp/tags/list
# {"name":"myapp","tags":["v1.0.0","v1.1.0","latest"]}
# Récupérer le manifest d'un tag
curl -u admin:password \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://registry.local:5000/v2/myapp/manifests/v1.0.0

Voici les erreurs les plus fréquentes lors du déploiement d'une registry Distribution et leurs solutions.

SymptômeCause probableSolution
http: server gave HTTP response to HTTPS clientRegistry non TLSConfigurer TLS ou ajouter à insecure-registries
x509: certificate signed by unknown authorityCertificat auto-signé non reconnuCopier le CA dans /etc/docker/certs.d/
unauthorized: authentication requiredCredentials incorrectsVérifier htpasswd et docker login
manifest unknownTag inexistantVérifier le nom exact avec l'API /tags/list
denied: requested access is deniedPermissions insuffisantesVérifier la config auth dans config.yml
Espace disque pleinBlobs orphelinsExécuter le garbage collection
  1. Distribution est la registry officielle CNCF, le même code que celui utilisé par Docker Hub. Simple, léger (~50 MB RAM), idéal pour le dev et les petits clusters.

  2. TLS est obligatoire pour un accès réseau. Utilisez Let's Encrypt avec Traefik ou des certificats auto-signés avec configuration des clients.

  3. L'authentification htpasswd suffit pour la plupart des cas. Pour du RBAC avancé, passez à Harbor ou utilisez un serveur de tokens externe.

  4. Le garbage collection libère l'espace des blobs orphelins. Planifiez-le régulièrement, registry arrêtée ou en read-only.

  5. Pour la production enterprise, préférez Harbor : interface web, scan de sécurité, réplication, RBAC. Distribution reste excellent comme registry cache ou pour le dev.

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn