Introduction
Déboguer une application conteneurisée ressemble souvent à résoudre un mystère. Les processus sont isolés de l'hôte, les systèmes de fichiers sont éphémères et le réseau ajoute des couches d'indirection. Lorsque votre application web ne répond plus ou plante, le chemin vers la résolution est rarement évident. Pour déboguer efficacement, vous devez comprendre l'architecture sous-jacente au débogage des conteneurs Docker : les composants impliqués, la circulation des données entre eux et les défaillances courantes. Ce guide fournit cette vue architecturale, accompagnée d'exemples pratiques, de commandes et d'extraits de configuration que vous pouvez utiliser immédiatement.
Le débogage des conteneurs Docker n'est pas un outil unique, mais une combinaison de fonctionnalités intégrées de Docker et d'utilitaires externes. Les composants principaux comprennent l'interface de ligne de commande Docker, le démon Docker, l'environnement d'exécution de conteneurs (runc ou containerd), les pilotes de journalisation, les commandes exec et inspect, ainsi que les outils de surveillance. En comprenant l'interaction entre ces éléments, vous pourrez diagnostiquer les problèmes plus rapidement et éviter les pièges courants. Ce guide couvre l'inventaire des versions et de l'environnement, la configuration sécurisée, la vérification et les diagnostics, les modes de défaillance et une liste de contrôle opérationnelle.
Tout au long de cet article, nous utiliserons un conteneur d'application web simple comme exemple concret. Il s'agit d'une application Node.js qui plante occasionnellement à cause d'exceptions non gérées, et nous la déboguerons étape par étape. Toutes les commandes ont été testées sur Linux (Ubuntu 22.04) avec Docker Engine 24.0, mais elles devraient fonctionner avec des modifications mineures sur d'autres systèmes.
Inventaire des versions et de l'environnement
Avant de déboguer un conteneur, établissez une base de référence claire. Connaissez la version de Docker, l'image et l'environnement d'exécution du conteneur, ainsi que le système d'exploitation de l'hôte. Ces informations sont souvent essentielles lors de la recherche de problèmes connus ou de la détermination de la compatibilité.
Prérequis
- Docker Engine 20.10 ou version ultérieure (nous utilisons 24.0).
- Connaissance de base de la ligne de commande.
- Un conteneur à déboguer (nous utilisons webapp:1.0).
- Accès aux journaux du conteneur et capacité d'exécuter des commandes à l'intérieur des conteneurs.
Vérification de la version et des informations de Docker
Exécutez les commandes suivantes sur l'hôte :
docker version --format '{{.Server.Version}}'
Sortie attendue (la vôtre peut différer) :
24.0.5
docker info --format '{{.OperatingSystem}} {{.Architecture}} {{.CgroupDriver}}'
Sortie attendue :
Ubuntu 22.04.3 LTS x86_64 systemd
Inspection du conteneur
Identifiez le conteneur à déboguer :
docker ps -a --filter "name=webapp" --format "table {{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Names}}"
Exemple de sortie :
CONTAINER ID IMAGE STATUS NAMES
3f4a5b6c7d8e webapp:1.0 Up 10 minutes (unhealthy) webapp
Notez le statut « (unhealthy) » ; cela indique que le contrôle de santé du conteneur échoue. Nous examinerons cela plus tard.
Topologie
Dans une configuration de débogage Docker typique, on trouve :
- Hôte : exécute le démon Docker et fournit les ressources.
- Démon Docker (dockerd) : gère les conteneurs, les images, les réseaux et les volumes.
- Environnement d'exécution de conteneurs (containerd / runc) : crée et exécute réellement les conteneurs.
- Conteneur : le processus applicatif et ses dépendances.
- Pilote de journalisation : capture la sortie standard/erreur du conteneur et la transmet à une destination (json-file par défaut).
- Outils de débogage : docker exec, docker logs, docker inspect, outils système comme strace, tcpdump, etc.
Flux de données : l'application écrit sur la sortie standard/erreur -> environnement d'exécution du conteneur -> pilote de journalisation -> fichier journal ou point de terminaison distant. Flux de contrôle : l'utilisateur lance docker exec -> CLI Docker -> démon -> environnement d'exécution -> exécute la commande dans l'espace de noms du conteneur.
Chemin de configuration sécurisé
Avant d'apporter des modifications, mettez en place une configuration de débogage sécurisée. Cela implique d'activer une journalisation appropriée, de configurer des contrôles de santé et de s'assurer de pouvoir exécuter des commandes dans le conteneur sans perturber la production si possible. Nous procéderons de manière ciblée et réversible.
Configuration des pilotes de journalisation
Le pilote de journalisation par défaut de Docker est json-file, qui stocke les journaux sur l'hôte. Pour le débogage, il est souvent utile de configurer un autre pilote ou d'ajuster les options. Nous conserverons json-file mais définirons une taille maximale pour éviter de remplir le disque.
Créez un conteneur avec les options de journalisation :
docker run -d --name webapp \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
-p 8080:8080 \
webapp:1.0
Cela limite les fichiers journaux à 10 Mo chacun et conserve 3 fichiers après rotation. Si vous devez modifier la journalisation d'un conteneur existant, vous devez le recréer. Pour voir la configuration de journalisation actuelle :
docker inspect --format '{{.HostConfig.LogConfig.Type}} {{.HostConfig.LogConfig.Config}}' webapp
Sortie :
json-file map[max-file:3 max-size:10m]
Mise en place des contrôles de santé
Un contrôle de santé indique à Docker si l'application fonctionne correctement. Pour notre application Node.js, nous pouvons ajouter un point de terminaison HTTP de santé. Modifiez le Dockerfile pour inclure HEALTHCHECK :
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["node", "server.js"]
Reconstruisez l'image :
docker build -t webapp:1.1 .
Exécutez le nouveau conteneur :
docker run -d --name webapp-healthy \
--log-driver json-file --log-opt max-size=10m --log-opt max-file=3 \
-p 8080:8080 \
webapp:1.1
Vérifiez l'état de santé après une minute :
docker ps --format '{{.HealthStatus}}' --filter name=webapp-healthy
Attendu : healthy.
Activation de l'accès exec
Pour le débogage, vous devez exécuter des commandes à l'intérieur du conteneur. Assurez-vous que le conteneur n'est pas exécuté avec --read-only ou un profil de sécurité restreint qui empêche exec. Nous utiliserons docker exec pour un débogage interactif.
Vérification et diagnostics
Maintenant que le conteneur fonctionne avec une journalisation et des contrôles de santé appropriés, nous pouvons diagnostiquer les problèmes. Simulons une défaillance dans notre application Node.js : elle plante lorsqu'un certain point de terminaison est sollicité, à cause d'un rejet de promesse non géré.
Observation des journaux
Tout d'abord, vérifiez les journaux :
docker logs webapp-healthy
Vous pourriez voir quelque chose comme :
Listening on port 8080
...
(node:1) UnhandledPromiseRejectionWarning: Error: Something went wrong
at /app/server.js:15:11
...
Cela nous indique où se trouve l'erreur. Nous pouvons également suivre les journaux en direct :
docker logs -f --tail 50 webapp-healthy
Exécution de commandes dans le conteneur
Nous pouvons inspecter le processus en cours d'exécution et l'environnement :
docker exec webapp-healthy ps aux
Sortie attendue (simplifiée) :
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.5 723456 45678 ? Ssl 12:34 0:00 node server.js
root 12 0.0 0.0 15568 2024 ? Rs 12:35 0:00 ps aux
Si le conteneur est en mauvaise santé, nous pouvons inspecter les journaux du contrôle de santé :
docker inspect --format '{{json .State.Health}}' webapp-healthy
La sortie comprend les entrées de journal des exécutions du contrôle de santé.
Utilisation de docker inspect pour la configuration
docker inspect révèle une mine d'informations :
docker inspect webapp-healthy
Champs clés : NetworkSettings.IPAddress, Mounts, Config.Env, HostConfig.Binds, etc. Pour le débogage, vérifiez les paramètres réseau pour assurer le mappage des ports :
docker inspect --format '{{range $p, $conf := .NetworkSettings.Ports}}{{$p}} -> {{(index $conf 0).HostPort}}{{"\n"}}{{end}}' webapp-healthy
Sortie :
8080/tcp -> 8080
Copie de fichiers pour analyse
Parfois, vous devez extraire des fichiers d'un conteneur, comme des cœurs de mémoire ou des journaux écrits sur disque. Utilisez docker cp :
docker cp webapp-healthy:/app/server.js ./server.js
Débogage avec strace
Si le processus se comporte de manière étrange, strace peut tracer les appels système. D'abord, installez strace dans le conteneur (s'il n'est pas présent) :
docker exec -it webapp-healthy sh -c "apk add --no-cache strace"
Puis attachez-vous au processus :
docker exec -it webapp-healthy strace -p 1
Cela peut être lourd ; utilisez-le avec prudence sur les conteneurs de production.
Modes de défaillance et récupération
Comprendre les modes de défaillance courants vous aide à vous préparer et à récupérer des problèmes. Nous couvrirons les problèmes typiques : conteneur qui se termine immédiatement, conteneur qui ne répond pas, disque plein à cause des journaux et mauvaise configuration réseau.
Le conteneur se termine immédiatement
Si un conteneur démarre et se termine, vérifiez son code de sortie et ses journaux.
docker ps -a --filter "name=webapp" --format "{{.Status}}"
Exemple :
Exited (1) 10 seconds ago
Puis inspectez les journaux :
docker logs webapp
Souvent, c'est une dépendance manquante ou une erreur de configuration. Corrigez le Dockerfile ou le point d'entrée et redémarrez.
Conteneur qui ne répond pas
Si un conteneur se bloque et ne répond pas aux commandes, il peut être à court de mémoire ou avoir un processus en interblocage. Vérifiez l'utilisation des ressources :
docker stats --no-stream webapp-healthy
Exemple de sortie :
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
3f4a5b6c7d8e webapp-healthy 0.00% 45.2MiB / 3.8GiB 1.16% 2.1kB / 0B 0B / 0B 2
Si la mémoire est élevée, envisagez d'augmenter les limites ou de profiler l'application. Vous pouvez également utiliser docker exec pour exécuter des commandes de diagnostic, mais si le conteneur est vraiment bloqué, vous devrez peut-être le redémarrer.
Disque plein à cause des journaux
Si le disque de l'hôte se remplit, la journalisation des conteneurs peut en être la cause. Vérifiez l'utilisation du disque des journaux Docker :
sudo du -sh /var/lib/docker/containers/*/*-json.log
Si un fichier est énorme, faites-le tourner ou supprimez-le. Mieux, configurez la rotation des journaux dès le départ. Pour effacer les journaux sans redémarrer le conteneur, utilisez :
truncate -s 0 /var/lib/docker/containers/<container-id>/<container-id>-json.log
Obtenez l'ID du conteneur avec docker ps -q.
Mauvaise configuration réseau
Si l'application ne peut pas se connecter à d'autres services, inspectez le réseau :
docker network inspect bridge
Vérifiez si les conteneurs sont sur le même réseau. Pour les réseaux personnalisés, créez-en un :
docker network create mynet
Puis exécutez les conteneurs avec --network mynet. Utilisez docker exec pour tester la connectivité :
docker exec webapp-healthy curl http://database:5432
Étapes de récupération
- Pour une récupération immédiate, redémarrez le conteneur : docker restart webapp-healthy.
- Si le problème persiste, recréez le conteneur avec une configuration actualisée.
- Pour les bugs de code, corrigez l'application et reconstruisez l'image.
Retour en arrière
Conservez les versions d'image précédentes pour pouvoir revenir en arrière :
docker tag webapp:1.1 webapp:stable
docker run -d --name webapp-stable ... webapp:stable
Si la nouvelle version échoue, revenez à l'image stable.
Pièges courants et comment les éviter
Même les utilisateurs expérimentés de Docker tombent dans des pièges prévisibles lors du débogage des conteneurs. Voici les plus courants, pourquoi ils surviennent et comment les éviter ou s'en remettre.
Piège 1 : Négliger la rotation des journaux
Pourquoi cela arrive : La configuration de journalisation par défaut stocke toute la sortie du conteneur dans un seul fichier JSON sans limite de taille. Avec le temps, ce fichier peut remplir le disque de l'hôte, provoquant des défaillances à l'échelle du système.
Comment éviter : Définissez toujours des options de rotation des journaux lors de l'exécution des conteneurs, soit via les options --log-opt, soit dans le fichier de configuration du démon Docker (/etc/docker/daemon.json). Par exemple, ajoutez ceci à daemon.json :
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Puis redémarrez le démon Docker.
Récupération : Si le disque est déjà plein, identifiez les gros fichiers journaux avec sudo du -sh /var/lib/docker/containers//-json.log et tronquez-les comme indiqué dans la section précédente. Ensuite, mettez en place la rotation pour éviter la récidive.
Piège 2 : Ignorer l'état du contrôle de santé
Pourquoi cela arrive : De nombreux développeurs ne configurent pas de contrôles de santé pour leurs conteneurs. Sans eux, Docker ne peut pas signaler si l'application fonctionne réellement, seulement si le processus est en cours d'exécution. Un conteneur peut être « Up » mais ne pas répondre.
Comment éviter : Définissez toujours un contrôle de santé dans votre Dockerfile ou lors de l'exécution du conteneur. Utilisez une commande légère qui vérifie la fonctionnalité essentielle de l'application, comme un point de terminaison HTTP ou un script simple. Par exemple :
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
Récupération : Si un conteneur est en mauvaise santé, inspectez les journaux de son contrôle de santé avec docker inspect --format '{{json .State.Health}}' <container> pour comprendre ce qui échoue.
Piège 3 : Modifier un conteneur en cours d'exécution au lieu de le recréer
Pourquoi cela arrive : Il est tentant d'utiliser docker exec pour modifier un conteneur en cours d'exécution (par exemple, installer des paquets manquants) plutôt que de mettre à jour le Dockerfile et de recréer le conteneur. Cependant, ces modifications sont perdues lorsque le conteneur est redémarré ou remplacé, ce qui conduit à des environnements incohérents.
Comment éviter : Traitez les conteneurs comme immuables. Toute modification nécessaire doit être reflétée dans le Dockerfile ou le point d'entrée, et le conteneur doit être recréé. Utilisez docker exec uniquement pour les diagnostics, pas pour les changements persistants.
Récupération : Si vous avez déjà modifié un conteneur en cours d'exécution, documentez les modifications et reconstruisez immédiatement l'image avec ces modifications incorporées.
Piège 4 : Négliger l'isolation réseau
Pourquoi cela arrive : Les conteneurs sur des réseaux différents ne peuvent pas communiquer par défaut. Les développeurs supposent souvent que tous les conteneurs peuvent se joindre, ce qui conduit à des erreurs de connexion refusée.
Comment éviter : Définissez explicitement des réseaux et attachez les conteneurs qui doivent communiquer. Utilisez des réseaux personnalisés pour une meilleure isolation et une résolution DNS. Par exemple :
docker network create backend
docker run -d --network backend --name db postgres
docker run -d --network backend --name webapp myapp
Récupération : Si un conteneur ne peut pas atteindre un autre service, vérifiez docker network inspect <network> pour voir si les deux conteneurs sont sur le même réseau. Sinon, recréez-en un avec le bon indicateur --network.
Piège 5 : Exécuter des conteneurs en tant que root
Pourquoi cela arrive : De nombreuses images de base exécutent les processus en tant que root par défaut. Si l'application est compromise, l'attaquant obtient les privilèges root à l'intérieur du conteneur, ce qui peut potentiellement mener à une compromission de l'hôte.
Comment éviter : Créez toujours un utilisateur non root dans votre Dockerfile et exécutez l'application en tant que cet utilisateur. Par exemple :
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
Récupération : Si vous avez des conteneurs qui s'exécutent en tant que root, prévoyez de les reconstruire avec un utilisateur non root. C'est particulièrement important pour les environnements de production.
Piège 6 : Utiliser l'étiquette latest en production
Pourquoi cela arrive : L'utilisation de l'étiquette « latest » facilite le tirage de l'image la plus récente, mais ce n'est pas déterministe. Le contenu de l'image peut changer sans avertissement, ce qui entraîne un comportement inattendu et rend le retour en arrière difficile.
Comment éviter : Utilisez des étiquettes de version explicites pour toutes les images, et envisagez d'utiliser des empreintes d'image pour une immuabilité absolue. Par exemple, utilisez myapp:1.4.2 au lieu de myapp:latest.
Récupération : Si vous avez utilisé latest, commencez à étiqueter les images avec des versions spécifiques à l'avenir. Vous pouvez également utiliser docker image inspect myapp:latest pour trouver l'empreinte et l'étiqueter avec une version pour les déploiements actuels.
Liste de contrôle opérationnelle
Utilisez cette liste de contrôle pour le débogage et la maintenance de routine. Chaque élément a un propriétaire (un rôle responsable unique, pas un groupe) et une fréquence d'examen pour assurer la responsabilisation.
| Étape | Action | Commande/Extrait | Propriétaire | Fréquence d'examen |
|---|---|---|---|---|
| 1 | Vérifier la version et l'environnement de Docker | docker version et docker info | Ingénieur DevOps (par ex., Priya Shah) | Mensuelle ou après les mises à jour de l'hôte |
| 2 | Lister les conteneurs en cours d'exécution avec leur statut | docker ps -a | Ingénieur de garde | Quotidienne ou par incident |
| 3 | Vérifier les journaux du conteneur | docker logs <container> --tail 100 | Propriétaire de l'application (par ex., Alex Chen) | À chaque déploiement ou incident |
| 4 | Inspecter la configuration du conteneur | docker inspect <container> | Ingénieur DevOps | Lorsque la configuration change |
| 5 | Vérifier l'état de santé | docker inspect --format '{{.State.Health.Status}}' <container> | Ingénieur de garde | Surveillance continue (avec alertes) |
| 6 | Voir l'utilisation des ressources | docker stats --no-stream | SRE (par ex., Maria Garcia) | Hebdomadaire ou en cas de problème de performance |
| 7 | Exécuter des commandes de diagnostic dans le conteneur | docker exec -it <container> sh | Développeur de garde | Uniquement pendant le débogage |
| 8 | Vérifier la configuration de journalisation | docker inspect --format '{{.HostConfig.LogConfig.Type}}' <container> | Ingénieur DevOps | Après la création du conteneur ou mensuellement |
| 9 | Tester la connectivité réseau depuis le conteneur | docker exec <container> ping <target> | Ingénieur réseau (si disponible) ou de garde | Lorsque des problèmes réseau sont signalés |
| 10 | Sauvegarder ou copier les fichiers importants du conteneur | docker cp <container>:<path> <local-path> | Propriétaire des données | Avant la suppression ou la migration du conteneur |
| 11 | Nettoyer les conteneurs et images inutilisés | docker system prune (avec prudence) | Ingénieur DevOps | Hebdomadaire (planifiée) |
| 12 | Documenter les conclusions et mettre à jour les procédures | - | Répondant à l'incident | Après chaque incident significatif |
Conclusion
Le débogage des conteneurs Docker nécessite une approche systématique fondée sur la compréhension de l'architecture. En inventoriant votre environnement, en configurant la journalisation et les contrôles de santé de manière sécurisée, en utilisant les commandes de diagnostic de manière efficace et en vous préparant aux défaillances courantes, vous pouvez résoudre les problèmes rapidement et prévenir les futurs. Les commandes et configurations de ce guide fournissent une base solide. Commencez par appliquer ces techniques à vos conteneurs actuels et construisez une liste de contrôle de débogage adaptée à vos applications. N'oubliez pas de versionner les images pour un retour en arrière facile et de toujours surveiller les journaux et l'état de santé. Avec la pratique, le débogage des conteneurs devient une routine gérable plutôt qu'un mystère.