Réponse courte : pour tirer une image d'un registre GitLab privé depuis Kubernetes, créez un deploy token avec le scope read_registry sur le projet ou le groupe, transformez-le en secret de type kubernetes.io/dockerconfigjson dans le namespace cible, puis référencez ce secret dans imagePullSecrets (du pod ou, mieux, du ServiceAccount). Trois commandes suffisent ; le reste de cet article traite du périmètre à choisir, de la rotation et du diagnostic des erreurs ImagePullBackOff.
C'est l'un des blocages les plus fréquents quand une équipe passe de docker compose sur un serveur unique à un cluster Kubernetes : le pipeline pousse bien l'image dans registry.gitlab.com/mon-groupe/mon-app, mais le cluster, lui, n'a aucune identité pour aller la chercher. Les pods tournent en boucle, les logs sont vides, et kubectl describe affiche un laconique unauthorized: HTTP Basic: Access denied.

Pourquoi Kubernetes ne peut pas lire votre registre GitLab
Le GitLab Container Registry est privé par défaut : sa visibilité suit celle du projet. Un projet privé implique un registre authentifié, et le kubelet — c'est lui qui tire les images, pas le plan de contrôle — n'hérite d'aucun ~/.docker/config.json de votre poste. Même si docker pull fonctionne chez vous après un docker login, le cluster reste anonyme.
Kubernetes résout ce problème avec un objet Secret de type kubernetes.io/dockerconfigjson. Le kubelet lit ce secret, en extrait les identifiants, et les présente au registre pendant le pull. Point essentiel, souvent découvert dans la douleur : un secret est propre à un namespace. Un secret créé dans default est invisible depuis staging ou production. Il faut donc le recréer dans chaque namespace qui déploie l'image.
Quel identifiant utiliser ?
GitLab propose plusieurs jetons, et tous ne se valent pas pour cet usage.
| Identifiant | Périmètre | Durée | Adapté au pull cluster ? |
|---|---|---|---|
| Deploy token (projet) | Un projet | Expiration optionnelle | Oui — le choix par défaut |
| Deploy token (groupe) | Tous les projets du groupe | Expiration optionnelle | Oui, si plusieurs apps du même groupe |
| Project access token | Un projet, scopes larges | Expiration obligatoire | Possible, mais surdimensionné |
| Personal access token | Tous les projets de l'utilisateur | Expiration obligatoire | Non — lié à une personne |
CI_JOB_TOKEN | Le job en cours | Quelques minutes | Non — expire avec le job |
Le deploy token gagne pour trois raisons : il n'est rattaché à aucun être humain (pas de rupture quand quelqu'un quitte l'équipe), il accepte le scope read_registry seul, et il peut être créé au niveau du groupe pour couvrir plusieurs microservices avec une seule paire identifiant/mot de passe.
Le CI_JOB_TOKEN, lui, est très pratique pour le docker login dans le pipeline, mais sa durée de vie correspond à celle du job. Un secret Kubernetes construit avec lui marchera au premier déploiement puis cassera au premier redémarrage de pod — un piège classique et particulièrement vicieux, puisque le symptôme apparaît des heures après la mise en production.
Créer le deploy token
Dans GitLab : Settings → Repository → Deploy tokens pour un projet, ou Settings → Repository → Deploy tokens au niveau du groupe. Remplissez :
- Name :
k8s-prod-pull(nommez par cluster/environnement, pas par application) - Expiration date : mettez-en une. Un an au maximum ; six mois est un bon compromis si vous automatisez la rotation.
- Username : laissez vide pour obtenir un nom généré (
gitlab+deploy-token-123456), ou fixez-le pour simplifier vos scripts. - Scopes : cochez uniquement
read_registry. Pasread_repository, paswrite_registry.
Copiez le mot de passe immédiatement : GitLab ne le réaffiche jamais.
Astuce : pour une image stockée dans le Dependency Proxy ou dans un package registry, le scope nécessaire diffère (
read_package_registry). Pour des images Docker classiques,read_registrysuffit.
Générer l'imagePullSecret
La commande impérative est la plus sûre, parce qu'elle construit le JSON à votre place :
kubectl create secret docker-registry gitlab-registry \
--namespace=production \
--docker-server=registry.gitlab.com \
--docker-username='gitlab+deploy-token-123456' \
--docker-password='gldt-xxxxxxxxxxxxxxxxxxxx' \
--docker-email='devops@exemple.fr'
Trois détails qui font échouer la moitié des tentatives :
--docker-serverdoit être l'hôte du registre, pas celui de GitLab. Sur gitlab.com c'estregistry.gitlab.com. Sur une instance auto-hébergée, c'est la valeur deregistry_external_urldansgitlab.rb, souventregistry.exemple.fr— jamaisgitlab.exemple.fr. Vérifiez-la en lisant le préfixe affiché sur la page Deploy → Container Registry du projet.- Pas de schéma, pas de chemin.
https://registry.gitlab.com/mon-groupeproduit un secret inopérant : le kubelet compare l'hôte de l'image à la clé duconfig.json. - Guillemets simples autour du mot de passe. Les jetons GitLab peuvent contenir des caractères interprétés par le shell.
Contrôlez le résultat sans le dévoiler dans l'historique :
kubectl -n production get secret gitlab-registry \
-o jsonpath='{.data.\.dockerconfigjson}' | base64 -d | jq '.auths | keys'
Vous devez voir exactement ["registry.gitlab.com"].
Rattacher le secret
Deux approches. Au niveau du pod, explicitement :
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
template:
spec:
imagePullSecrets:
- name: gitlab-registry
containers:
- name: api
image: registry.gitlab.com/mon-groupe/mon-app:1.4.2
Ou, plus élégant, au niveau du ServiceAccount utilisé par le namespace — tous les pods qui l'utilisent en héritent, sans toucher aux manifestes :
kubectl -n production patch serviceaccount default \
-p '{"imagePullSecrets":[{"name":"gitlab-registry"}]}'
La seconde option évite les oublis quand une nouvelle application arrive, mais elle est moins explicite à la lecture. Sur les clusters que nous exploitons pour nos clients, nous patchons le ServiceAccount et laissons imagePullSecrets dans les charts Helm : la redondance est inoffensive et sert de documentation.

Une bonne pratique : imagePullPolicy et tags immuables
Tant que vous y êtes, réglez la politique de pull. Kubernetes applique IfNotPresent par défaut, sauf pour un tag :latest où il bascule sur Always. Conséquence : avec :latest, un redéploiement tire une image différente sans que rien ne le signale dans Git, et un secret expiré casse le pod immédiatement.
La règle que nous appliquons systématiquement :
- taguez par version sémantique ou par SHA court de commit (
:1.4.2,:sha-a1b2c3d) ; - gardez
imagePullPolicy: IfNotPresent; - activez les tags protégés et immuables côté GitLab pour empêcher l'écrasement d'un tag déjà déployé.
Une image taguée par SHA rend aussi le rollback trivial : kubectl rollout undo deployment/api repointe vers une couche déjà présente dans le cache du nœud, sans dépendre du registre.
Automatiser depuis GitLab CI/CD
Créer le secret à la main une fois par namespace fonctionne, jusqu'au jour de l'expiration du token. Deux stratégies tiennent dans le temps.
Stratégie 1 : recréation idempotente dans le pipeline
Stockez le deploy token dans des variables CI/CD masquées et protégées (K8S_PULL_USER, K8S_PULL_TOKEN), puis régénérez le secret à chaque déploiement :
deploy:
stage: deploy
image: bitnami/kubectl:1.34
environment: production
rules:
- if: $CI_COMMIT_TAG
script:
- kubectl create secret docker-registry gitlab-registry
--namespace="$KUBE_NAMESPACE"
--docker-server="$CI_REGISTRY"
--docker-username="$K8S_PULL_USER"
--docker-password="$K8S_PULL_TOKEN"
--dry-run=client -o yaml | kubectl apply -f -
- helm upgrade --install api ./charts/api
--namespace "$KUBE_NAMESPACE"
--set image.tag="$CI_COMMIT_TAG"
--atomic --timeout 5m
Le couple --dry-run=client -o yaml | kubectl apply -f - est la façon canonique de rendre kubectl create secret idempotent : il met à jour le secret s'il existe, le crée sinon, et ne renvoie jamais AlreadyExists. $CI_REGISTRY est fourni automatiquement par GitLab et contient déjà le bon hôte, ce qui élimine l'erreur n° 1 décrite plus haut.
Le --atomic de Helm vous évite de laisser un Deployment à moitié déployé si le pull échoue malgré tout : le chart revient à son état précédent.
Stratégie 2 : secret géré hors pipeline
Sur un cluster multi-équipes, mieux vaut sortir le jeton du CI. Deux outils remplissent ce rôle sans effort :
- External Secrets Operator : le token vit dans AWS Secrets Manager, Vault ou Scaleway Secret Manager, et un objet
ExternalSecretle matérialise endockerconfigjsondans chaque namespace. La rotation se fait dans le coffre, le cluster suit automatiquement au prochain cycle de rafraîchissement. - Reflector ou kubernetes-replicator : vous maintenez un seul secret source dans un namespace d'administration et une annotation le réplique dans tous les namespaces autorisés. Simple, et très efficace quand la seule douleur est la multiplication des namespaces.
Dans les deux cas, notez la date d'expiration du deploy token dans votre runbook. GitLab envoie un courriel d'avertissement avant l'échéance des tokens qui en ont une, mais ce message arrive souvent dans la boîte d'un seul propriétaire de projet.
Diagnostiquer un ImagePullBackOff
Quand ça casse, suivez cet ordre. Il isole la cause en moins de cinq minutes.
# 1. Lire le message exact de l'événement
kubectl -n production describe pod api-7d9c8b5f4-abcde | tail -30
# 2. Vérifier que le secret existe bien DANS ce namespace
kubectl -n production get secret gitlab-registry
# 3. Vérifier que le pod le référence réellement
kubectl -n production get pod api-7d9c8b5f4-abcde \
-o jsonpath='{.spec.imagePullSecrets}'
Lisez ensuite le message :
| Message | Cause probable | Correctif |
|---|---|---|
unauthorized: HTTP Basic: Access denied | Token expiré, révoqué ou mauvais scope | Recréer le deploy token avec read_registry |
pull access denied ... may require authentication | Secret absent du namespace ou non référencé | Créer le secret dans le bon namespace, patcher le ServiceAccount |
manifest unknown / not found | Le tag n'existe pas (typo, pipeline de build échoué) | Vérifier le tag dans Container Registry |
x509: certificate signed by unknown authority | Registre auto-hébergé avec certificat interne | Ajouter l'autorité de certification sur les nœuds |
toomanyrequests | Limites de taux (registre amont, image de base) | Dependency Proxy ou miroir de cache |
Pour rejouer le pull manuellement avec les mêmes identifiants, hors de Kubernetes :
echo "$K8S_PULL_TOKEN" | docker login registry.gitlab.com \
-u 'gitlab+deploy-token-123456' --password-stdin
docker pull registry.gitlab.com/mon-groupe/mon-app:1.4.2
Si cette commande réussit depuis votre poste mais échoue dans le cluster, le problème n'est plus l'authentification : regardez du côté du réseau sortant des nœuds (proxy d'entreprise, règles de sortie, politique réseau) ou de l'espace disque du nœud — un /var/lib/containerd saturé produit des échecs de pull trompeurs, visibles via la condition DiskPressure de kubectl describe node.

Le coût du registre, souvent oublié
Le stockage du GitLab Container Registry est facturé sur le quota de stockage du namespace GitLab, et il grossit vite : chaque pipeline pousse une image complète. Avec une image Node.js de 350 Mo et quarante pipelines par semaine, vous ajoutez plus de 50 Go par mois si rien ne nettoie.
Deux leviers concrets :
- Les règles d'expiration des tags (Settings → Packages and registries → Clean up image tags) : conservez les 10 derniers tags, gardez ceux correspondant à des versions
v*, supprimez le reste au-delà de 30 jours. Attention, la purge ne libère réellement l'espace qu'après le passage du garbage collector du registre. - Des images plus petites : une base
node:22-alpineou un build multi-étapes avecnpm ci --omit=devfait fondre l'image de plusieurs centaines de mégaoctets, et accélère d'autant le pull sur chaque nouveau nœud.
Le second levier a un effet direct sur votre temps de démarrage en cas de montée en charge : un pod qui tire 80 Mo au lieu de 350 Mo démarre nettement plus vite quand l'autoscaler ajoute un nœud vierge. C'est exactement le genre d'optimisation que nous intégrons aux prestations d'infrastructure et de déploiement que nous menons pour nos clients, aux côtés du développement applicatif lui-même.
Questions fréquentes
Faut-il un secret par namespace ?
Oui. Les secrets Kubernetes ne traversent pas les namespaces. Soit vous recréez le secret dans chacun (via le pipeline, de façon idempotente), soit vous utilisez un réplicateur comme Reflector ou External Secrets Operator pour le propager depuis une source unique.
Peut-on utiliser CI_JOB_TOKEN pour l'imagePullSecret ?
Non, pas pour un secret durable. Ce jeton expire à la fin du job. Il est parfait pour docker login pendant le build, mais un pod redémarré quelques heures plus tard ne pourra plus tirer l'image. Utilisez un deploy token.
Un deploy token de groupe ou de projet ?
Projet si une seule application utilise le registre, groupe dès que vous déployez plusieurs services du même groupe sur le même cluster. Le token de groupe réduit le nombre de secrets à faire tourner ; en contrepartie, il élargit le périmètre de compromission. Dans tous les cas : scope read_registry seulement.
Comment éviter la coupure le jour de l'expiration ?
Créez le nouveau token avant de supprimer l'ancien, appliquez le nouveau secret dans tous les namespaces, vérifiez un kubectl rollout restart sur une application non critique, puis révoquez l'ancien. Et notez l'échéance dans votre calendrier d'exploitation plutôt que de compter sur le courriel d'alerte de GitLab.
Et avec un registre auto-hébergé derrière un certificat interne ?
Le secret est identique, mais le kubelet doit faire confiance à l'autorité de certification. Installez-la dans le magasin système des nœuds (/usr/local/share/ca-certificates/ puis update-ca-certificates, redémarrage de containerd) ou, selon le runtime, configurez /etc/containerd/certs.d/<hôte>/. Modifier le pod n'y changera rien : le pull se fait au niveau du nœud.
En résumé
- Le registre GitLab est privé par défaut ; le kubelet a besoin d'un secret
kubernetes.io/dockerconfigjsonpour s'y authentifier. - Utilisez un deploy token (projet ou groupe) avec le seul scope
read_registry, jamais un jeton personnel niCI_JOB_TOKEN. --docker-servervaut l'hôte du registre (registry.gitlab.comou$CI_REGISTRY), sanshttps://ni chemin de projet.- Un secret par namespace : rendez sa création idempotente avec
--dry-run=client -o yaml | kubectl apply -f -, ou propagez-le via External Secrets Operator. - Rattachez-le au ServiceAccount pour couvrir tous les pods, et gardez
imagePullSecretsdans vos charts à titre documentaire. - Taguez par version ou par SHA, pas en
:latest, avecimagePullPolicy: IfNotPresentet des tags immuables. - En cas d'
ImagePullBackOff, lisez le message dekubectl describe: il distingue un token invalide (Access denied) d'un tag inexistant (manifest unknown). - Nettoyez le registre (règles d'expiration des tags) et réduisez la taille des images : moins de stockage facturé, démarrages plus rapides.
Un déploiement qui coince sur un pull d'image, un cluster à industrialiser ou une chaîne GitLab CI/CD à reprendre ? Parlez-nous de votre contexte — notre équipe travaille sur ces sujets au quotidien depuis Pontault-Combault.