← Retour aux tutoriels

Déboguer les variables d’environnement et l’injection de configuration dans des apps Dockerisées

dockerdocker composevariables d’environnementconfigurationdébogagesecrets.envdevops

Déboguer les variables d’environnement et l’injection de configuration dans des apps Dockerisées

Les variables d’environnement sont l’un des mécanismes les plus utilisés pour configurer une application conteneurisée : URLs de bases de données, clés d’API, modes dev/prod, options de logs, etc. Pourtant, c’est aussi une source fréquente de bugs : variable absente, valeur écrasée, différence entre build et runtime, confusion entre .env et ENV, encodage, espaces, expansion inattendue par le shell, ou encore injection via orchestrateur (Compose, Swarm, Kubernetes).

Ce tutoriel explique comment diagnostiquer et corriger ces problèmes, avec des commandes réelles, des cas concrets et une méthode reproductible.


1) Comprendre où “vivent” les variables dans Docker

Avant de déboguer, il faut distinguer plusieurs “couches” de configuration :

  1. Variables au moment du build

    • ARG dans un Dockerfile
    • Valeurs passées via docker build --build-arg ...
    • Utiles pour paramétrer des étapes de build (téléchargement, compilation, activation d’options), mais pas destinées à être des secrets.
  2. Variables au moment du runtime

    • ENV dans le Dockerfile (valeurs par défaut dans l’image)
    • docker run -e (valeurs injectées au lancement)
    • --env-file (fichier de variables injectées au lancement)
    • Docker Compose (environment, env_file)
    • Orchestrateurs (Kubernetes ConfigMap/Secret → env, Swarm secrets/configs)
  3. Variables dans le processus

    • Une fois le conteneur lancé, les variables existent dans l’environnement du processus PID 1 (et de ses enfants), visibles via printenv, /proc/1/environ, etc.
  4. Configuration applicative

    • L’application peut lire l’environnement directement, ou via une bibliothèque, ou via un fichier généré (template), ou via un système de configuration interne (Spring, Django, Node config, etc.).
    • Certaines apps lisent l’environnement au démarrage uniquement (pas de rechargement à chaud).

Point clé : un bug peut venir d’un mauvais endroit. Exemple classique : vous passez --build-arg API_URL=... et vous vous attendez à ce que l’app le voie au runtime, alors que ARG n’est pas automatiquement présent dans l’environnement d’exécution.


2) Outils de base pour inspecter l’environnement d’un conteneur

2.1 Lister les variables d’environnement au runtime

Démarrer un conteneur éphémère et afficher l’environnement :

docker run --rm alpine:3.20 printenv | sort

Inspecter un conteneur déjà lancé :

docker exec -it mon-conteneur printenv | sort

Si printenv n’existe pas (image minimaliste), utiliser env :

docker exec -it mon-conteneur env | sort

2.2 Vérifier ce que Docker pense injecter

Afficher la configuration d’un conteneur (incluant Env) :

docker inspect mon-conteneur --format '{{json .Config.Env}}' | jq

Sans jq :

docker inspect mon-conteneur --format '{{.Config.Env}}'

Voir les variables au niveau image (valeurs ENV du Dockerfile) :

docker image inspect mon-image:tag --format '{{json .Config.Env}}' | jq

2.3 Lire l’environnement du PID 1 (utile si exec est limité)

Sur Linux, l’environnement d’un processus est dans /proc/<pid>/environ (séparé par des \0) :

docker exec -it mon-conteneur sh -lc 'tr "\0" "\n" < /proc/1/environ | sort'

C’est utile quand l’entrée printenv est “polluée” par un shell intermédiaire, ou quand vous suspectez que le processus principal n’a pas les mêmes variables que votre session docker exec.


3) Différences fondamentales : ARG vs ENV

3.1 ARG : seulement pendant le build

Exemple de Dockerfile :

FROM alpine:3.20

ARG API_URL
RUN echo "API_URL au build = ${API_URL}"
CMD ["sh", "-lc", "echo API_URL au runtime = $API_URL; sleep 3600"]

Build :

docker build -t demo-arg --build-arg API_URL=https://api.exemple.local .

Run :

docker run --rm demo-arg

Vous verrez que API_URL est affiché au build, mais au runtime il est vide (sauf si vous l’injectez via -e).

3.2 ENV : valeur par défaut dans l’image (runtime)

FROM alpine:3.20
ENV API_URL=https://api.defaut.local
CMD ["sh", "-lc", "echo API_URL=$API_URL; sleep 3600"]

Puis :

docker build -t demo-env .
docker run --rm demo-env

Surcharge au lancement :

docker run --rm -e API_URL=https://api.prod.local demo-env

Règle pratique :


4) Pièges fréquents et comment les diagnostiquer

4.1 Variable définie mais “vide” : expansion par le shell hôte

Sur votre machine, si vous faites :

export API_URL=https://api.local
docker run --rm -e API_URL alpine:3.20 sh -lc 'echo $API_URL'

Ça marche. Mais si vous écrivez :

docker run --rm -e API_URL=$API_URL alpine:3.20 sh -lc 'echo $API_URL'

Ça dépend de votre shell hôte : -e API_URL=$API_URL est évalué avant docker run. Si API_URL n’est pas exportée (ou vide), vous injectez une valeur vide.

Diagnostic :

4.2 Espaces, guillemets et caractères spéciaux

Avec docker run -e, évitez les espaces non intentionnels :

docker run --rm -e "TOKEN=abc def" alpine:3.20 env

Ici la valeur contient un espace, donc il faut guillemeter.
Pour des valeurs contenant $ (ex. mot de passe) :

docker run --rm -e 'PASSWORD=p@$$w0rd' alpine:3.20 sh -lc 'echo "$PASSWORD"'

Utiliser des quotes simples côté hôte empêche l’expansion.

4.3 .env n’est pas magique (et dépend de l’outil)

Exemple avec docker run :

cat > app.env <<'EOF'
APP_MODE=dev
API_URL=https://api.local
EOF

docker run --rm --env-file app.env alpine:3.20 sh -lc 'echo "$APP_MODE $API_URL"'

4.4 Ordre de priorité et écrasements

Les valeurs peuvent être définies à plusieurs endroits. Une hiérarchie typique :

Diagnostic :

  1. Inspecter l’image : docker image inspect ...
  2. Inspecter le conteneur : docker inspect ...
  3. Inspecter le PID 1 : /proc/1/environ
  4. Vérifier le code/config de l’app : quel nom exact est lu ?

4.5 Variables “présentes” mais non prises en compte par l’application

Causes fréquentes :

Diagnostic :

docker exec -it mon-conteneur ps aux
docker inspect mon-conteneur --format '{{.Path}} {{join .Args " "}}'
docker exec -it mon-conteneur sh -lc 'tr "\0" "\n" < /proc/1/environ | grep -E "APP_|API_|DB_"'

5) Déboguer l’injection via Docker Compose (concepts et commandes)

Même si vous n’utilisez pas Compose partout, il est très courant en dev. Les problèmes typiques : confusion entre substitution de variables et injection dans le conteneur.

5.1 Substitution vs injection

Pour voir ce que Compose a réellement compris, utilisez :

docker compose config

C’est une commande essentielle : elle affiche la configuration “résolue” après substitution.

5.2 Vérifier les variables dans le conteneur lancé par Compose

Lister les conteneurs :

docker compose ps

Entrer dans un service :

docker compose exec monservice sh -lc 'printenv | sort'

Inspecter via Docker :

docker inspect $(docker compose ps -q monservice) --format '{{json .Config.Env}}' | jq

5.3 Erreurs fréquentes avec Compose

Diagnostic :


6) Déboguer un ENTRYPOINT/CMD qui modifie la configuration

Beaucoup d’images utilisent un script d’entrée pour :

6.1 Voir l’ENTRYPOINT et le CMD

docker image inspect mon-image:tag --format 'ENTRYPOINT={{json .Config.Entrypoint}} CMD={{json .Config.Cmd}}'

6.2 Surclasser l’entrypoint pour diagnostiquer

Lancer le conteneur en remplaçant l’entrypoint par un shell :

docker run --rm -it --entrypoint sh mon-image:tag

Puis inspecter l’environnement :

env | sort

Si l’image n’a pas sh (distroless), vous devrez utiliser une image de debug (voir section 9).

6.3 Tracer un script d’entrée

Si l’entrypoint est un script shell, vous pouvez parfois activer un mode verbeux en le lançant avec sh -x :

docker run --rm -it --entrypoint sh mon-image:tag -lc 'sh -x /chemin/entrypoint.sh'

Ou, si l’entrypoint est déjà sh -c, vous pouvez injecter :

docker run --rm -it mon-image:tag sh -lc 'set -x; env; /chemin/cmd-original'

Objectif : repérer si le script fait quelque chose comme export VAR=... ou écrase une valeur.


7) Cas pratique : variable absente dans une app Node/Python/Java (méthode générale)

Même si les stacks diffèrent, la méthode de debug reste la même.

7.1 Reproduire minimalement

Lancer le conteneur avec un shell et afficher la variable :

docker run --rm -it -e APP_PORT=8080 mon-image:tag sh -lc 'echo "APP_PORT=$APP_PORT"'

Si la variable est présente, le problème est côté app (nom, lecture, moment de lecture).

7.2 Vérifier le nom exact attendu

Exemples typiques :

Chercher dans l’image (si les sources sont présentes) :

docker run --rm -it mon-image:tag sh -lc 'grep -R "process.env" -n /app 2>/dev/null | head'

Ou afficher la doc de l’app.

7.3 Vérifier le moment de lecture

Si l’app lit au démarrage, changer une variable via docker exec ne sert à rien :

docker exec -it mon-conteneur sh -lc 'export FEATURE_X=true'

Cela ne modifie que la session interactive, pas le processus déjà lancé.

La correction consiste à injecter au lancement (ou redémarrer) :

docker rm -f mon-conteneur
docker run -d --name mon-conteneur -e FEATURE_X=true mon-image:tag

8) Secrets, logs et “fuites” : déboguer sans compromettre

8.1 Ne pas exposer les secrets par erreur

Évitez :

Pour déboguer, filtrez :

docker exec -it mon-conteneur sh -lc 'printenv | grep -E "^(APP_|DB_|API_)" | sed -E "s/(PASSWORD|TOKEN|SECRET)=.*/\1=***REDACTED***/"'

8.2 Attention à l’historique shell

Si vous tapez -e PASSWORD=... en clair, cela peut rester dans l’historique (~/.bash_history). Préférez --env-file avec des permissions strictes :

chmod 600 app.env
docker run --rm --env-file app.env mon-image:tag

9) Images minimalistes (distroless, scratch) : stratégies de debug

Certaines images n’ont ni shell, ni env, ni cat. Déboguer devient plus difficile.

9.1 Utiliser docker inspect

Même sans shell, vous pouvez voir les variables configurées :

docker inspect mon-conteneur --format '{{json .Config.Env}}' | jq

9.2 Injecter un “debug sidecar” via namespace (approche Docker)

Sur Docker “pur”, on peut lancer un conteneur de debug dans le même namespace réseau et parfois le même PID namespace (selon options), mais c’est limité. Une approche simple : lancer un conteneur avec la même image mais en surchargeant l’entrypoint (si possible). Si impossible, utilisez une variante debug de l’image (recommandé).

9.3 Construire une image debug temporaire

Si votre image finale est distroless, gardez une cible “debug” avec un shell.

Exemple (conceptuel) :

Puis :

docker build -t mon-app:debug --target debug .
docker run --rm -it --entrypoint sh mon-app:debug

L’idée : déboguer avec une image plus riche, tout en déployant une image minimaliste.


10) Problèmes subtils : encodage, retours chariot, fichiers .env Windows

10.1 Retours chariot CRLF (\r) dans les valeurs

Si un fichier .env a été édité sous Windows, vous pouvez vous retrouver avec \r à la fin :

Symptôme : mot de passe “correct” mais auth échoue, URL invalide, etc.

Détecter dans le conteneur :

docker run --rm --env-file app.env alpine:3.20 sh -lc 'printf "%q\n" "$API_URL"'

Ou afficher les caractères invisibles :

docker run --rm --env-file app.env alpine:3.20 sh -lc 'echo "$API_URL" | cat -A'

Corriger côté hôte :

sed -i 's/\r$//' app.env

10.2 BOM UTF-8 au début du fichier

Un BOM peut casser la première clé. Détecter :

xxd -g 1 -l 16 app.env

Si vous voyez ef bb bf, supprimez-le avec un éditeur ou :

tail -c +4 app.env > app.env.nobom && mv app.env.nobom app.env

11) Méthode systématique de diagnostic (checklist)

Quand “la variable n’est pas prise en compte”, suivez cette séquence :

  1. Confirmer la valeur côté hôte (si injection depuis le shell)

    echo "API_URL=$API_URL"
  2. Confirmer la méthode d’injection

    • docker run -e ?
    • --env-file ?
    • Compose environment/env_file ?
    • Variable définie dans l’image via ENV ?
  3. Voir ce que Docker a configuré

    docker inspect mon-conteneur --format '{{json .Config.Env}}' | jq
  4. Voir ce que le processus principal reçoit

    docker exec -it mon-conteneur sh -lc 'tr "\0" "\n" < /proc/1/environ | sort'
  5. Vérifier l’entrypoint (écrasement, génération de config)

    docker inspect mon-conteneur --format '{{.Path}} {{join .Args " "}}'
  6. Vérifier l’application

    • nom exact de la variable
    • moment de lecture (au démarrage)
    • fallback/valeur par défaut
    • logs de démarrage
    • fichier de config généré
  7. Vérifier les caractères invisibles (CRLF, espaces, BOM)

    • cat -A, printf %q, sed -i 's/\r$//'

12) Bonnes pratiques pour éviter les bugs d’injection

12.1 Normaliser les noms et documenter

12.2 Valider au démarrage (fail fast)

Dans un script shell d’entrypoint, vous pouvez refuser de démarrer si une variable manque :

: "${DATABASE_URL:?DATABASE_URL est obligatoire}"
: "${API_KEY:?API_KEY est obligatoire}"

Dans une app, faites l’équivalent (et loggez clairement).

12.3 Éviter d’utiliser ARG pour des secrets

Un ARG peut se retrouver dans l’historique de build (couches, cache, logs CI). Préférez :

12.4 Préférer --env-file en local

C’est plus reproductible et évite les expansions surprises :

docker run --rm --env-file .env.local mon-image:tag

12.5 Garder une cible “debug” et des outils minimaux

Avoir sh, env, cat, grep dans une image debug fait gagner un temps énorme.


13) Session de debug complète (exemple guidé)

Supposons : votre app n’arrive pas à se connecter à la base, vous pensez que DATABASE_URL n’est pas correcte.

  1. Vérifier comment vous lancez :
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Command}}'
  1. Inspecter les variables configurées :
docker inspect mon-conteneur --format '{{json .Config.Env}}' | jq
  1. Vérifier la valeur exacte dans le processus :
docker exec -it mon-conteneur sh -lc 'tr "\0" "\n" < /proc/1/environ | grep -E "^DATABASE_URL=" | cat -A'
  1. Si vous voyez \r$ :
# côté hôte
sed -i 's/\r$//' app.env
docker rm -f mon-conteneur
docker run -d --name mon-conteneur --env-file app.env mon-image:tag
  1. Confirmer dans les logs applicatifs :
docker logs --tail=200 mon-conteneur
  1. Si l’app lit une autre variable (ex. DB_URL) : corriger l’injection.

Conclusion

Déboguer les variables d’environnement dans Docker revient à tracer le chemin de la valeur : de votre shell/CI → Docker/Compose → conteneur → processus → application. Les outils clés sont docker inspect, printenv/env, la lecture de /proc/1/environ, et (avec Compose) docker compose config. En appliquant une checklist stricte et en vous méfiant des pièges (ARG vs ENV, substitution vs injection, CRLF/BOM, écrasements par entrypoint), vous transformez un problème “mystique” en diagnostic mécanique et rapide.

Si vous me dites votre contexte (Docker run ou Compose, langage de l’app, extrait de Dockerfile/commande de lancement et le nom des variables), je peux proposer une procédure de debug ciblée et une correction minimale.