← Retour aux tutoriels

Résoudre les erreurs type ImagePullBackOff en production Docker (sans Kubernetes)

dockerdevopsregistryimage-pulltroubleshootingproductiontlsdns

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 :

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 :

Causes :

Vérifications :

  1. Lister les tags (selon votre registry) :

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
  1. Vérifier l’architecture :
docker manifest inspect mon-registry.exemple.com/monapp:1.2.3 | head -n 80
uname -m

Corrections :


3.2 Authentification: unauthorized / denied / pull access denied

Symptômes :

Causes :

Diagnostic :

  1. Tester un login interactif :
docker login mon-registry.exemple.com
  1. Vérifier l’identité utilisée (cas cloud) :
  1. 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 :


3.3 Rate limiting (Docker Hub) : toomanyrequests

Symptômes :

Causes :

Corrections :

  1. S’authentifier à Docker Hub :
docker login
  1. 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.

  2. Éviter :latest et les pulls inutiles (voir section “stratégies” plus bas).


3.4 Problèmes réseau : DNS, proxy, firewall, MTU, timeouts TLS

Symptômes :

Diagnostic réseau :

  1. DNS :
cat /etc/resolv.conf
getent hosts mon-registry.exemple.com
dig mon-registry.exemple.com +trace | head -n 60
  1. 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
  1. Vérifier si un proxy est imposé :
env | grep -i proxy || true
systemctl show docker --property=Environment
  1. Vérifier les règles firewall :
sudo iptables -S | head -n 80
sudo nft list ruleset | head -n 120

Corrections :

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 :

Causes :

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) :

  1. 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
  1. 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 :

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 :

  1. 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
  1. Si vous devez libérer plus :

3.7 Concurrence / verrous / pulls parallèles (CI, redéploiements)

Symptômes :

Diagnostic :

ss -tpn | head
iostat -xz 1 5 || true

Corrections :


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 :


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 :

Inconvénient :


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”

  1. Pré-pull de la nouvelle version
  2. Vérification
  3. 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 :

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 :

Préférez :

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 :


10) Prévenir les incidents : pratiques de production

10.1 Ne pas dépendre d’Internet en prod (quand c’est possible)

10.2 Tagging et immutabilité

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 :

Exemples de signaux :


11) Scénarios concrets et résolution pas à pas

Scénario A : unauthorized: authentication required sur un registry privé

  1. Pull manuel :
docker pull mon-registry.exemple.com/monapp:1.2.3
  1. Login :
docker login mon-registry.exemple.com
  1. Retenter :
docker pull mon-registry.exemple.com/monapp:1.2.3
  1. Si ça marche en SSH mais pas via systemd :

Scénario B : x509: certificate signed by unknown authority

  1. 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
  1. 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
  1. Retester :
docker pull mon-registry.exemple.com/monapp:1.2.3

Scénario C : manifest unknown

  1. Vérifier le tag :
docker pull mon-registry.exemple.com/monapp:1.2.3
  1. Vérifier côté CI : l’image a-t-elle été push ?
  1. Corriger le tag dans la config de déploiement.

Scénario D : rate limit Docker Hub

  1. Confirmer l’erreur :
docker pull nginx:latest
  1. Login :
docker login
  1. 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)

  1. Toujours diagnostiquer avec un docker pull manuel pour obtenir l’erreur exacte.
  2. Standardiser l’auth registry (robot + déploiement de config.json ou tokens cloud).
  3. Installer proprement les CAs internes (OS + /etc/docker/certs.d).
  4. Éviter latest, préférer tags versionnés et idéalement digests.
  5. Mettre en place un pré-pull avant redémarrage/bascule.
  6. Réduire la dépendance à Docker Hub via un cache/registry interne.
  7. 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.