Résoudre les erreurs type ImagePullBackOff en production Docker (sans Kubernetes)
Les erreurs de type ImagePullBackOff sont souvent associées à Kubernetes, mais elles décrivent en réalité un problème plus général : un runtime tente de récupérer une image de conteneur et échoue, puis réessaie avec un délai croissant. En production “Docker pur” (Docker Engine, Docker Compose, Swarm, systemd, scripts CI/CD), on rencontre exactement les mêmes symptômes : un service ne démarre pas, un conteneur reste en boucle de redémarrage, ou un déploiement se bloque parce que l’image n’est pas téléchargeable.
Ce tutoriel explique en profondeur comment diagnostiquer et corriger ces échecs de “pull” d’images en production sans Kubernetes, avec des commandes réelles, des causes fréquentes, et des stratégies robustes pour éviter que cela se reproduise.
1) Comprendre ce que signifie “ImagePullBackOff” côté Docker
Dans Kubernetes, ImagePullBackOff signifie : “échec de téléchargement de l’image, et le système attend avant de réessayer”.
Dans un environnement Docker (ou Compose/Swarm), vous verrez plutôt :
pull access deniedmanifest unknownunauthorized: authentication requiredx509: certificate signed by unknown authorityTLS handshake timeoutno such hostconnection refusedtoomanyrequests: You have reached your pull rate limit- un service qui reste en
0/1ouRestarting
La logique est la même : le moteur Docker n’arrive pas à obtenir l’image, donc il ne peut pas créer le conteneur (ou il redémarre en boucle si l’image est supprimée/invalidée).
2) Checklist rapide de diagnostic (les 5 minutes qui sauvent)
Avant de creuser, exécutez ces commandes sur la machine de prod concernée.
2.1 Vérifier la version et l’état du daemon Docker
docker version
docker info
systemctl status docker --no-pager
journalctl -u docker -n 200 --no-pager
Cherchez des indices : erreurs réseau, DNS, certificats, proxy, storage driver, etc.
2.2 Vérifier si l’image existe localement
docker images --digests | head -n 50
docker image inspect mon-registry.exemple.com/monapp:1.2.3 >/dev/null && echo "OK local" || echo "Absent"
Si l’image est déjà locale, le problème n’est pas un “pull” (ou alors un pull forcé).
2.3 Tenter un pull manuel (et observer l’erreur exacte)
docker pull mon-registry.exemple.com/monapp:1.2.3
Copiez l’erreur exacte : c’est souvent la clé.
2.4 Vérifier la résolution DNS et la connectivité vers le registry
getent hosts mon-registry.exemple.com
nslookup mon-registry.exemple.com || true
dig +short mon-registry.exemple.com || true
curl -v https://mon-registry.exemple.com/v2/ 2>&1 | tail -n 50
Un registry Docker répond généralement sur /v2/ (souvent 401 Unauthorized est normal si vous n’êtes pas authentifié ; l’important est d’avoir une réponse TLS/HTTP cohérente).
2.5 Vérifier l’authentification Docker
docker logout mon-registry.exemple.com || true
docker login mon-registry.exemple.com
cat ~/.docker/config.json
En production, l’auth doit être gérée proprement (on y revient).
3) Causes fréquentes et corrections (avec commandes)
3.1 Tag inexistant, mauvais nom d’image, ou “manifest unknown”
Symptômes :
manifest unknown: manifest unknownnot foundpull access denied(parfois ambigu)
Causes :
- Tag mal orthographié (
:latestvs:Latest) - Image poussée sur un autre registry / namespace
- Pipeline CI qui n’a pas push l’image
- Architecture non disponible (ex : image uniquement amd64, machine arm64)
Vérifications :
- Lister les tags (selon votre registry) :
- Docker Hub : via API (souvent limité)
- GitLab Registry, Harbor, Artifactory : UI ou API
- Registry v2 “nu” : nécessite endpoints spécifiques
Pour un registry v2 compatible, vous pouvez déjà vérifier le “catalog” (souvent désactivé) :
curl -s https://mon-registry.exemple.com/v2/_catalog | head
Vérifier un manifest précis (nécessite parfois auth) :
curl -I -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://mon-registry.exemple.com/v2/monapp/manifests/1.2.3
- Vérifier l’architecture :
docker manifest inspect mon-registry.exemple.com/monapp:1.2.3 | head -n 80
uname -m
Corrections :
- Corriger le tag dans votre déploiement.
- Publier le tag manquant.
- Publier une image multi-arch :
docker buildx create --use docker buildx build --platform linux/amd64,linux/arm64 \ -t mon-registry.exemple.com/monapp:1.2.3 \ --push .
3.2 Authentification: unauthorized / denied / pull access denied
Symptômes :
unauthorized: authentication requireddenied: requested access to the resource is deniedpull access denied for ...
Causes :
- Pas de
docker loginsur l’hôte - Token expiré (ECR/GCR/ACR)
- Mauvais “scope” (droits insuffisants)
~/.docker/config.jsonabsent ou non accessible (service systemd exécuté sous un autre utilisateur)- Environnement CI qui déploie sans propager les credentials
Diagnostic :
- Tester un login interactif :
docker login mon-registry.exemple.com
- Vérifier l’identité utilisée (cas cloud) :
- AWS ECR :
aws sts get-caller-identity aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin <account>.dkr.ecr.eu-west-1.amazonaws.com - GCP Artifact Registry :
gcloud auth list gcloud auth configure-docker europe-west1-docker.pkg.dev - Azure ACR :
az account show az acr login --name monacr
- Vérifier le fichier de config Docker :
cat ~/.docker/config.json
Point crucial en prod : si votre service est lancé par systemd, l’utilisateur effectif n’est pas forcément votre user SSH. Le docker login fait sous deploy ne sert à rien si le pull est fait sous un autre contexte.
Corrections robustes :
- Utiliser un compte “robot”/“deploy” dédié au registry, avec droits read-only.
- Déployer des credentials via un mécanisme contrôlé :
docker loginexécuté par le même utilisateur que celui qui déclenche les pulls.- ou fournir
DOCKER_CONFIGà l’unité systemd (exemple plus bas).
- Pour ECR, automatiser le renouvellement du token (valide 12h) :
while true; do aws ecr get-login-password --region eu-west-1 \ | docker login --username AWS --password-stdin <account>.dkr.ecr.eu-west-1.amazonaws.com sleep 39600 # 11h done
3.3 Rate limiting (Docker Hub) : toomanyrequests
Symptômes :
toomanyrequests: You have reached your pull rate limit
Causes :
- Trop de pulls anonymes depuis la même IP
- Auto-scaling / redéploiements fréquents
- CI/CD qui pull en boucle
Corrections :
- S’authentifier à Docker Hub :
docker login
-
Mettre en place un registry miroir/cache (fortement recommandé). Exemple avec un registry “proxy cache” (si supporté par votre solution) ou un Harbor/Artifactory en cache.
-
Éviter
:latestet les pulls inutiles (voir section “stratégies” plus bas).
3.4 Problèmes réseau : DNS, proxy, firewall, MTU, timeouts TLS
Symptômes :
no such hostTLS handshake timeouti/o timeoutconnection reset by peerproxyconnect tcp: ...
Diagnostic réseau :
- DNS :
cat /etc/resolv.conf
getent hosts mon-registry.exemple.com
dig mon-registry.exemple.com +trace | head -n 60
- Routage et connectivité :
ip route
ping -c 3 mon-registry.exemple.com || true
curl -vk https://mon-registry.exemple.com/v2/ 2>&1 | tail -n 80
- Vérifier si un proxy est imposé :
env | grep -i proxy || true
systemctl show docker --property=Environment
- Vérifier les règles firewall :
sudo iptables -S | head -n 80
sudo nft list ruleset | head -n 120
Corrections :
- Corriger DNS (ex : systemd-resolved, resolv.conf, DNS interne).
- Autoriser la sortie vers le registry (ports 443/5000 selon cas).
- Configurer correctement le proxy pour Docker daemon via systemd drop-in :
Créer un drop-in :
sudo systemctl edit docker
Ajouter :
[Service]
Environment="HTTP_PROXY=http://proxy.exemple.com:3128"
Environment="HTTPS_PROXY=http://proxy.exemple.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1,.exemple.com,10.0.0.0/8"
Puis :
sudo systemctl daemon-reload
sudo systemctl restart docker
journalctl -u docker -n 100 --no-pager
3.5 Certificats TLS et registries privés : x509 / CA inconnue
Symptômes :
x509: certificate signed by unknown authoritycertificate is valid for ... not ...- erreurs SNI / CN / SAN
Causes :
- Registry interne avec certificat auto-signé
- CA interne non installée sur l’hôte
- Certificat expiré
- Nom DNS utilisé ne correspond pas au SAN
Diagnostic :
openssl s_client -connect mon-registry.exemple.com:443 -servername mon-registry.exemple.com </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates -ext subjectAltName
Corrections recommandées (propres) :
- Installer la CA interne dans le trust store de l’OS (Debian/Ubuntu) :
sudo cp ma-ca-interne.crt /usr/local/share/ca-certificates/ma-ca-interne.crt
sudo update-ca-certificates
- Ajouter le certificat au trust Docker pour CE registry :
Docker lit des CAs spécifiques dans :
/etc/docker/certs.d/<host>:<port>/ca.crt
Exemple :
sudo mkdir -p /etc/docker/certs.d/mon-registry.exemple.com
sudo cp ma-ca-interne.crt /etc/docker/certs.d/mon-registry.exemple.com/ca.crt
sudo systemctl restart docker
Option de dernier recours (à éviter en prod) : registry “insecure”.
Dans /etc/docker/daemon.json :
sudo cat >/etc/docker/daemon.json <<'EOF'
{
"insecure-registries": ["mon-registry.exemple.com:5000"]
}
EOF
sudo systemctl restart docker
Cela désactive des garanties TLS et doit être évité si possible.
3.6 Espace disque, inode, ou couche corrompue : pulls qui échouent “au milieu”
Symptômes :
no space left on devicefailed to register layer: ...write ...: no space left- erreurs overlay2
Diagnostic :
df -h
df -i
docker system df
docker info | grep -E 'Storage Driver|Docker Root Dir'
Voir les logs daemon :
journalctl -u docker -n 200 --no-pager
Corrections :
- Nettoyage prudent :
docker ps -a
docker image ls
docker volume ls
Puis :
docker system prune -f
docker image prune -a -f
docker volume prune -f
- Si vous devez libérer plus :
- Déplacer
Docker Root Dirvers un disque plus grand (opération structurante). - Mettre en place une politique de rétention d’images.
3.7 Concurrence / verrous / pulls parallèles (CI, redéploiements)
Symptômes :
- pulls qui se bloquent, timeouts, erreurs intermittentes
- services qui démarrent une fois sur deux
Diagnostic :
- Vérifier si plusieurs jobs déploient en même temps.
- Observer l’activité réseau et disque :
ss -tpn | head
iostat -xz 1 5 || true
Corrections :
- Sérialiser les déploiements sur un hôte.
- Pré-puller l’image avant de redémarrer les services (pattern “pull puis switch”).
- Utiliser des tags immuables (voir plus bas).
4) Cas Docker Compose : erreurs de pull et stratégies de redémarrage
Avec Docker Compose, un docker compose up peut tenter de pull si l’image n’est pas locale.
4.1 Voir exactement ce que Compose fait
docker compose config
docker compose pull --ignore-pull-failures
docker compose up -d
docker compose logs -f --tail=200
4.2 Forcer un pull contrôlé (et échouer vite)
En prod, il est souvent préférable de pull explicitement et d’arrêter si ça échoue :
set -euo pipefail
docker compose pull
docker compose up -d
4.3 Éviter les surprises de latest
Si vous utilisez :latest, vous pouvez déployer une version non testée sans le vouloir, et vous rendre dépendant d’un pull à chaque redémarrage.
Bonnes pratiques :
- Taguer par version (
:1.2.3) ou par SHA (:git-<sha>). - Utiliser des digests (voir section 6).
5) Cas Docker Swarm : équivalent “ImagePullBackOff” côté services
En Swarm, les nœuds doivent pull l’image. Si un nœud n’y arrive pas, la tâche échoue et Swarm réessaie.
5.1 Diagnostiquer un service
docker service ls
docker service ps monservice --no-trunc
docker service inspect monservice --pretty
Regardez les colonnes ERROR et l’historique des tâches.
5.2 Vérifier les logs et événements
docker service logs monservice --tail 200
docker events --since 1h | grep -i -E 'pull|image|error' | tail -n 100
5.3 Problème classique : credentials non propagés aux nœuds
Si vous faites docker login seulement sur le manager, les workers peuvent échouer.
Solution : créer le service avec --with-registry-auth (attention à la gestion des secrets) :
docker service update --with-registry-auth --image mon-registry.exemple.com/monapp:1.2.3 monservice
Ou lors de la création :
docker service create --with-registry-auth --name monservice mon-registry.exemple.com/monapp:1.2.3
6) La solution la plus robuste : déployer par digest (immuable)
Les tags peuvent être réassignés. Un digest (@sha256:...) pointe vers un contenu exact.
6.1 Obtenir le digest
Après un pull :
docker pull mon-registry.exemple.com/monapp:1.2.3
docker image inspect --format='{{index .RepoDigests 0}}' mon-registry.exemple.com/monapp:1.2.3
Vous obtiendrez quelque chose comme :
mon-registry.exemple.com/monapp@sha256:abcd...
6.2 Déployer en utilisant le digest
Exemple en ligne de commande :
docker run -d --name monapp mon-registry.exemple.com/monapp@sha256:abcd1234...
Avantages :
- reproductibilité parfaite
- évite les “surprises” si un tag est repushé
- facilite les rollbacks
Inconvénient :
- nécessite une gestion de release qui publie et enregistre les digests.
7) Mettre en place une procédure de déploiement “pull-safe” (sans Kubernetes)
Objectif : ne jamais casser la prod parce qu’un pull échoue au mauvais moment.
7.1 Pattern recommandé : “pré-pull + bascule”
- Pré-pull de la nouvelle version
- Vérification
- Redémarrage/bascule
Exemple de script bash :
#!/usr/bin/env bash
set -euo pipefail
IMAGE="mon-registry.exemple.com/monapp:1.2.3"
SERVICE_NAME="monapp"
echo "[1/3] Pull de l'image: $IMAGE"
docker pull "$IMAGE"
echo "[2/3] Vérification de l'image"
docker image inspect "$IMAGE" >/dev/null
echo "[3/3] Redémarrage du conteneur"
if docker ps -a --format '{{.Names}}' | grep -qx "$SERVICE_NAME"; then
docker rm -f "$SERVICE_NAME"
fi
docker run -d --restart=always --name "$SERVICE_NAME" -p 8080:8080 "$IMAGE"
Ce pattern garantit que vous ne supprimez pas l’ancien conteneur tant que le pull n’a pas réussi (vous pouvez encore améliorer en gardant l’ancien conteneur actif et en basculant via un reverse proxy).
7.2 Healthcheck applicatif et rollback
Docker permet un HEALTHCHECK dans l’image, mais sans Kubernetes vous devez orchestrer le rollback.
Approche simple :
- Démarrer le nouveau conteneur sur un port différent
- Tester une URL
- Basculer le proxy (nginx/traefik) ou remplacer l’ancien conteneur
Exemple de test :
curl -fsS http://127.0.0.1:18080/health
8) Observabilité : où lire les erreurs exactes
8.1 Logs du daemon Docker
journalctl -u docker -f
8.2 Événements Docker (très utile pour voir les pulls)
docker events --since 30m
Filtrer :
docker events --since 30m | grep -i -E 'pull|image|error|fail'
8.3 Logs d’un conteneur qui redémarre
docker ps
docker logs --tail 200 monapp
docker inspect monapp --format '{{.State.Status}} {{.State.ExitCode}} {{.State.Error}}'
Attention : si le conteneur ne se crée pas (image manquante), docker logs ne servira pas ; il faut les logs daemon.
9) Sécuriser les credentials registry en production (sans bricolage)
9.1 Éviter les logins manuels sur serveurs
Un docker login manuel sur un serveur de prod :
- est difficile à auditer
- peut expirer
- dépend de l’utilisateur local
- peut être perdu après reprovisionnement
Préférez :
- un compte robot
- un secret manager (Vault, AWS Secrets Manager, etc.)
- une étape de provisioning (Ansible, cloud-init) qui installe le
config.jsonau bon endroit
9.2 Utiliser DOCKER_CONFIG pour isoler les credentials
Vous pouvez stocker la config Docker dans un répertoire dédié :
sudo mkdir -p /etc/docker-auth
sudo cp config.json /etc/docker-auth/config.json
sudo chmod 600 /etc/docker-auth/config.json
Puis exécuter vos pulls avec :
DOCKER_CONFIG=/etc/docker-auth docker pull mon-registry.exemple.com/monapp:1.2.3
Dans un service systemd, vous pouvez définir :
Environment=DOCKER_CONFIG=/etc/docker-auth
10) Prévenir les incidents : pratiques de production
10.1 Ne pas dépendre d’Internet en prod (quand c’est possible)
- Héberger un registry interne (Harbor, GitLab Registry, Artifactory, Nexus).
- Répliquer/cacher les images externes.
- Éviter que chaque redémarrage doive “pull” sur Docker Hub.
10.2 Tagging et immutabilité
- Interdire le repush d’un tag de release (
1.2.3doit rester1.2.3). - Utiliser des digests pour les déploiements critiques.
- Conserver un historique (au moins N versions) sur le registry.
10.3 Pré-pull lors des maintenances
Avant une fenêtre de maintenance :
docker pull mon-registry.exemple.com/monapp:1.2.3
docker pull mon-registry.exemple.com/sidecar:4.5.6
10.4 Alerting
Surveillez :
- échecs de
docker pulldans les logs - taux d’erreurs réseau
- espace disque Docker
Exemples de signaux :
journalctl -u dockercontientunauthorized,x509,toomanyrequestsdf -hdépasse 80%docker system dfexplose
11) Scénarios concrets et résolution pas à pas
Scénario A : unauthorized: authentication required sur un registry privé
- Pull manuel :
docker pull mon-registry.exemple.com/monapp:1.2.3
- Login :
docker login mon-registry.exemple.com
- Retenter :
docker pull mon-registry.exemple.com/monapp:1.2.3
- Si ça marche en SSH mais pas via systemd :
- vérifier l’utilisateur du service
- ajouter
DOCKER_CONFIGdans l’unité - ou faire le login dans le bon contexte
Scénario B : x509: certificate signed by unknown authority
- Vérifier le certificat présenté :
openssl s_client -connect mon-registry.exemple.com:443 -servername mon-registry.exemple.com </dev/null \
| openssl x509 -noout -issuer -subject -dates
- Installer la CA :
sudo cp ma-ca.crt /usr/local/share/ca-certificates/ma-ca.crt
sudo update-ca-certificates
sudo mkdir -p /etc/docker/certs.d/mon-registry.exemple.com
sudo cp ma-ca.crt /etc/docker/certs.d/mon-registry.exemple.com/ca.crt
sudo systemctl restart docker
- Retester :
docker pull mon-registry.exemple.com/monapp:1.2.3
Scénario C : manifest unknown
- Vérifier le tag :
docker pull mon-registry.exemple.com/monapp:1.2.3
- Vérifier côté CI : l’image a-t-elle été push ?
- Sur la machine CI :
docker push mon-registry.exemple.com/monapp:1.2.3 - Vérifier le digest publié :
docker buildx imagetools inspect mon-registry.exemple.com/monapp:1.2.3
- Corriger le tag dans la config de déploiement.
Scénario D : rate limit Docker Hub
- Confirmer l’erreur :
docker pull nginx:latest
- Login :
docker login
- Mieux : mirroring interne + pinning de versions :
docker pull mon-registry-interne.exemple.com/library/nginx:1.27
12) Résumé opérationnel (ce que vous devez standardiser)
- Toujours diagnostiquer avec un
docker pullmanuel pour obtenir l’erreur exacte. - Standardiser l’auth registry (robot + déploiement de
config.jsonou tokens cloud). - Installer proprement les CAs internes (OS +
/etc/docker/certs.d). - Éviter
latest, préférer tags versionnés et idéalement digests. - Mettre en place un pré-pull avant redémarrage/bascule.
- Réduire la dépendance à Docker Hub via un cache/registry interne.
- Surveiller logs Docker, espace disque, et erreurs réseau.
Annexes : commandes utiles “copier-coller”
Inspecter une image et ses digests
docker image inspect mon-registry.exemple.com/monapp:1.2.3 | head -n 40
docker image inspect --format '{{json .RepoDigests}}' mon-registry.exemple.com/monapp:1.2.3
Nettoyage (prudence)
docker system df
docker system prune -f
docker image prune -a -f
docker volume prune -f
Debug réseau rapide
getent hosts mon-registry.exemple.com
curl -vk https://mon-registry.exemple.com/v2/
Voir les erreurs côté daemon
journalctl -u docker -n 300 --no-pager
docker events --since 1h | tail -n 200
Si vous me donnez le message d’erreur exact (sortie complète de docker pull ...) et votre contexte (Compose, Swarm, systemd, registry privé/cloud), je peux proposer un plan de correction ciblé et une procédure de durcissement adaptée à votre production.