Priorité des variables d’environnement Docker : pourquoi votre configuration n’est jamais celle que vous croyez

Cet article vous a aidé ?

Vous déployez un conteneur. Il démarre. Il fonctionne. Mais il ne se comporte pas comme prévu. Les logs indiquent qu’il se connecte à la mauvaise base, utilise un niveau de log erroné ou se bind sur le mauvais port. Vous regardez votre docker-compose.yml comme si on vous avait menti en face.

C’est probablement arrivé — accidentellement, à cause de la priorité. Docker et Docker Compose ont plusieurs couches capables de définir « la même » variable d’environnement, et la valeur retenue n’est souvent pas celle que vous pensiez. La solution ne relève pas du héroïsme. Il s’agit d’apprendre les règles, puis d’instrumenter la réalité.

Le modèle mental : trois problèmes distincts liés aux « variables d’environnement »

La plupart des confusions autour des variables d’environnement Docker viennent du mélange de trois mécanismes séparés qui paraissent liés :

1) Environnement d’exécution du conteneur (ce que le processus voit réellement)

Il s’agit de l’ensemble des variables à l’intérieur du conteneur en cours d’exécution, visibles par le PID 1 et ses process via env ou /proc/1/environ. Elles proviennent de la configuration du conteneur Docker, qui elle-même peut être issue des valeurs par défaut de l’image, de Compose, de flags CLI, et d’autres sources.

2) Interpolation dans le fichier Compose (ce que Compose substitue dans le YAML)

Compose utilise aussi les variables d’environnement sur votre hôte pour remplacer les placeholders ${VAR} dans le YAML. Cette substitution se produit avant la création du conteneur. Ce n’est pas la même chose que définir l’environnement d’exécution du conteneur.

C’est là que beaucoup se brûlent : ils définissent environment: dans Compose et supposent que cela affectera aussi tout ${VAR} ailleurs dans le fichier. Ce n’est pas le cas.

3) Couches de configuration de l’application (ce que votre appli choisit d’honorer)

Même quand Docker est correct, votre application peut écraser les valeurs : fichiers de config, flags, valeurs par défaut, conventions de frameworks, ou une bibliothèque qui lit les variables avec un préfixe, ignore les valeurs vides, ou considère "false" comme truthy parce que non vide. Docker ne peut pas vous protéger contre cela.

Si vous voulez un slogan pour la production : les variables d’environnement Docker ne sont pas « un réglage ». Ce sont une entrée. Les entrées ont des règles de priorité. Les entrées sont ignorées. Les entrées dérivent.

Faits et histoire : comment nous en sommes arrivés là

  • Docker n’a pas inventé la configuration par variables d’environnement ; le modèle « 12-factor app » a popularisé les env vars comme configuration au début des années 2010, et les conteneurs ont rendu cela quasi-universel.
  • L’instruction ENV dans les Dockerfile précède Compose ; les auteurs d’images mettaient des valeurs par défaut bien avant que la plupart des équipes standardisent sur des fichiers Compose pour le déploiement.
  • L’interpolation de variables dans Compose s’est inspirée des habitudes shell : ${VAR}, valeurs par défaut, et « prendre depuis votre environnement actuel ». Pratique pour le dev, catastrophique pour la reproductibilité.
  • Les premières versions de Compose chargeaient automatiquement un fichier .env par défaut, et ce comportement est devenu une habitude — même quand les équipes ont ensuite séparé les fichiers d’env par environnement.
  • Docker a deux concepts de « fichier d’env » : le --env-file du CLI et le env_file: de Compose. Ils se ressemblent, mais ne sont pas interchangeables et ne se comportent pas identiquement dans les cas limites.
  • La spec OCI stocke Env dans la config d’image ; ENV dans un Dockerfile devient des métadonnées intégrées à l’image et participe à la fusion au runtime.
  • La gestion des secrets s’est démocratisée après des fuites via les env vars ; les variables d’environnement sont faciles à afficher, logger ou exposer via des endpoints de debug. « Pratique » n’est pas synonyme de « sûr ».
  • Compose a évolué vers une spec ; différentes implémentations (plugin docker compose vs ancien docker-compose) n’étaient pas toujours d’accord sur certains comportements. Les arêtes vives sont en grande partie limées maintenant, mais les habitudes legacy persistent.

Une vérité opérationnelle n’a pas changé : quand vous empilez plusieurs couches qui peuvent toutes définir « la même » valeur, vous finirez par livrer la mauvaise. Pas parce que vous êtes négligent. Parce que vous êtes humain et que le système accepte volontiers des entrées conflictuelles.

Cartographie des priorités : docker run, Dockerfile, Compose et interpolation

Dockerfile ENV vs overrides au runtime

Les auteurs d’images définissent des valeurs par défaut avec ENV dans le Dockerfile. Celles-ci deviennent le Config.Env de l’image. À l’exécution, Docker fusionne les variables d’environnement provenant de plusieurs sources. La règle à retenir :

Les paramètres runtime écrasent les valeurs par défaut de l’image.

Ainsi, si l’image contient ENV LOG_LEVEL=info et que vous lancez :

cr0x@server:~$ docker run --rm -e LOG_LEVEL=debug alpine:3.20 env | grep LOG_LEVEL
LOG_LEVEL=debug

Précédence avec docker run (vue pratique)

Lorsque vous démarrez un conteneur avec docker run, l’environnement effectif est essentiellement :

  1. Valeurs par défaut de l’image (ENV dans Dockerfile)
  2. Env depuis --env-file (si utilisé)
  3. Flags explicites -e KEY=VALUE qui écrasent les valeurs précédentes

Il y a des nuances (comme -e KEY signifiant « prendre la valeur de l’environnement client »), mais opérationnellement : explicite bat implicite.

Compose joue deux jeux de priorité en même temps

Compose complique les choses car il joue deux rôles :

  • Interpoler des variables dans le fichier Compose (côté hôte) : résolution de ${VAR}.
  • Définir l’environnement runtime du service (côté conteneur) : environment:, env_file:, plus ce qui est déjà dans l’image.

Précédence d’interpolation Compose (côté hôte)

Pour remplacer ${VAR} dans le YAML, Compose suit généralement cette intuition :

  1. Variables depuis l’environnement shell où vous lancez docker compose
  2. Variables depuis le fichier projet .env (si présent dans le répertoire de travail / répertoire du projet)
  3. Valeurs par défaut dans l’expression, comme ${VAR:-default}

Si vous ne retenez qu’une chose : environment: n’alimente pas l’interpolation. Si vous écrivez :

cr0x@server:~$ cat docker-compose.yml
services:
  api:
    image: alpine:3.20
    environment:
      DB_HOST: db
    command: ["sh", "-lc", "echo ${DB_HOST}"]

Ce ${DB_HOST} est résolu sur l’hôte, pas dans le conteneur. Si votre hôte n’a pas DB_HOST défini (et que votre .env non plus), vous afficherez une chaîne vide ou générerez un avertissement selon la version/settings de Compose.

Précédence de l’environnement runtime Compose (côté conteneur)

Pour l’environnement qui se retrouve dans le conteneur, l’ordre habituel des gagnants est :

  1. Valeurs par défaut image ENV
  2. Variables chargées via env_file:
  3. Variables définies dans environment: qui écrasent env_file
  4. Certaines implémentations permettent aussi des overrides CLI via docker compose run -e, qui battent le fichier

De plus : si vous spécifiez plusieurs entrées env_file, les fichiers les plus tardifs écrasent les précédents. C’est pratique pour empiler des couches. C’est aussi la façon dont vous pouvez accidentellement expédier des réglages de staging en production parce qu’un fichier a été réordonné dans un diff.

Null, vide et « présent mais vide »

Il y a une différence vicieuse entre :

  • Non défini : la variable n’existe pas dans l’environnement
  • Vide : la variable existe avec la valeur ""
  • Chaîne littérale « null » : existe et vaut null (courant lors du templating YAML)

Le YAML Compose peut exprimer des valeurs vides de façons qui semblent innocentes :

  • FOO: (vide)
  • FOO: "" (chaîne vide explicite)

Les applis traitent souvent « présent mais vide » comme « configuré », ce qui mène à des comportements indésirables. Pour des bases de données, des noms d’hôte vides peuvent se résoudre en localhost ou retomber sur des valeurs par défaut dans le code des librairies. Le conteneur est correct. L’appli « aide ».

Une citation pour rester lucide

L’espoir n’est pas une stratégie. — General Gordon R. Sullivan

La priorité des variables d’environnement est l’endroit où l’espoir vient mourir. Instrumentez-la à la place.

Où naît la dérive de configuration : les pièges qui font mal en production

Piège : .env n’est pas l’environnement du conteneur

Le fichier .env utilisé par Compose pour l’interpolation est une fonctionnalité de commodité. Il n’est pas injecté automatiquement dans le conteneur à moins que vous le référenciez explicitement avec env_file: ou que vous mappez des valeurs dans environment:.

C’est pourquoi « ça marchait sur mon portable » arrive : le portable a un .env dans le répertoire du projet, CI n’en a pas, et la production utilise un répertoire de travail différent ou exécute Compose depuis un script wrapper.

Piège : « J’ai changé la variable, pourquoi le conteneur en cours ne change-t-il pas ? »

Parce que les conteneurs ne sont pas des shells. Mettre à jour un fichier Compose ne patch pas à chaud l’environnement d’un conteneur existant. Vous devez recréer le conteneur (ou au moins redémarrer avec recréation, selon ce qui a changé).

Piège : Les healthchecks et les sidecars voient un monde différent

Les commandes de healthcheck s’exécutent à l’intérieur du conteneur, donc elles voient l’env conteneur. Mais si vous avez templatisé la commande de healthcheck via une interpolation côté hôte, vous avez peut-être injecté la mauvaise valeur lors de la création. Le check passe, puis le trafic en production échoue. Mon genre préféré d’incident.

Piège : Variables proxy et valeurs par défaut « utiles »

HTTP_PROXY, NO_PROXY et leurs semblables sont fréquemment définis sur des laptops d’entreprise, des agents de build, et même dans les unités systemd du daemon Docker. Ils influencent silencieusement le comportement de build et d’exécution. Vous vous retrouvez avec des conteneurs qui n’accèdent à Internet que dans certains environnements et personne ne sait pourquoi.

Blague #1 : Les variables d’environnement sont comme les commérages de bureau : elles se répandent partout, sont rarement documentées, et choisissent toujours le pire moment pour être vraies.

Piège : Votre appli a sa propre priorité

Patrons courants :

  • Frameworks qui privilégient les fichiers de config sur les env vars sauf si un switch « use env » est activé.
  • Librairies qui lisent DATABASE_URL si présent, sinon lisent DB_HOST/DB_USER, etc.
  • Applications qui traitent 0, false et no de manière inconsistante.

En production, vous ne voulez pas de « magie ». Vous voulez un contrat de configuration explicite : quelle variable l’emporte, et comment la valider au démarrage.

Tâches pratiques : commandes qui disent ce qui est réel (et quoi faire ensuite)

On ne débogue pas la priorité en lisant le YAML plus fort. On la débogue en demandant au runtime ce qu’il a effectivement fait. Ci-dessous des tâches pratiques que j’ai utilisées sur des systèmes réels, avec commandes, sorties représentatives et la décision à prendre.

Tâche 1 : Inspecter l’environnement effectif du conteneur (vérité rapide)

cr0x@server:~$ docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api-1 | sort | sed -n '1,10p'
DB_HOST=db-prod
LOG_LEVEL=info
PORT=8080
TZ=UTC

Ce que cela signifie : Ce sont les env vars enregistrées dans la config du conteneur. C’est ce que le processus obtient au démarrage.

Décision : Si la valeur est erronée ici, cessez d’accuser l’application. Corrigez Compose/les flags run/les defaults de l’image et recréez le conteneur.

Tâche 2 : Vérifier ce que le processus voit réellement (au cas où l’entrypoint modifie l’env)

cr0x@server:~$ docker exec api-1 sh -lc 'tr "\0" "\n" < /proc/1/environ | sort | grep -E "DB_HOST|LOG_LEVEL|PORT"'
DB_HOST=db-prod
LOG_LEVEL=info
PORT=8080

Ce que cela signifie : L’environnement du PID 1. Si cela diffère de docker inspect, quelque chose à l’intérieur du conteneur l’a modifié (script d’entrypoint, supervisor, etc.).

Décision : Si PID 1 diffère, auditez les scripts d’entrypoint et l’outillage de démarrage. C’est un problème d’appli/image, pas de Compose.

Tâche 3 : Voir la config Compose entièrement rendue (interpolation résolue)

cr0x@server:~$ docker compose config | sed -n '/services:/,/networks:/p' | sed -n '1,80p'
services:
  api:
    command:
    - sh
    - -lc
    - ./start-api
    environment:
      DB_HOST: db-prod
      LOG_LEVEL: info
    image: myorg/api:1.8.4

Ce que cela signifie : C’est l’interprétation finale de Compose, après fusion des fichiers et résolution des ${VAR}.

Décision : Si docker compose config affiche une valeur erronée, vos inputs d’interpolation sont incorrects (env shell, .env, defaults) ou vos fichiers d’override ne sont pas ce que vous croyez.

Tâche 4 : Lister les conteneurs et confirmer que vous déboguez le bon

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
NAMES     IMAGE              STATUS          PORTS
api-1     myorg/api:1.8.4    Up 2 hours      0.0.0.0:8080->8080/tcp
db-1      postgres:16        Up 2 hours      5432/tcp

Ce que cela signifie : Noms, images, uptime, ports. Les bugs de « mauvaise configuration » sont souvent des bugs de « mauvais conteneur ».

Décision : Si vous voyez plusieurs conteneurs aux noms similaires, confirmez le nom du projet et le contexte ; vous inspectez peut-être la stack de la semaine dernière.

Tâche 5 : Vérifier sur quel projet Compose vous opérez

cr0x@server:~$ docker compose ls
NAME            STATUS              CONFIG FILES
billing         running(6)          /srv/billing/docker-compose.yml
billing-dev     running(6)          /home/cr0x/billing/docker-compose.yml

Ce que cela signifie : Les noms de projet Compose font partie de l’identité. Même YAML, projet différent, conteneurs différents.

Décision : Si vous êtes dans le mauvais projet, arrêtez. Changez de répertoire, utilisez -p, ou nettoyez les doublons.

Tâche 6 : Vérifier si le conteneur a été recréé après des changements de config

cr0x@server:~$ docker inspect -f '{{.Name}} {{.Created}}' api-1
/api-1 2026-01-03T08:12:54.123456789Z

Ce que cela signifie : Timestamp de création. Si vous avez édité la config à 09:00 et que le conteneur a été créé à 08:12, rien n’a changé.

Décision : Recréez : docker compose up -d --force-recreate (ou au minimum up -d si Compose détecte une différence).

Tâche 7 : Confirmer quels fichiers env sont appliqués (et dans quel ordre)

cr0x@server:~$ docker compose config --services
api
db
cr0x@server:~$ grep -nE 'env_file|environment' -n docker-compose.yml
14:    env_file:
15:      - ./env/common.env
16:      - ./env/prod.env
17:    environment:
18:      LOG_LEVEL: info

Ce que cela signifie : Plusieurs fichiers env implique du layering. Les plus récents gagnent. environment: les écrase tous les deux.

Décision : Si vous avez besoin qu’une valeur soit vraiment souveraine, placez-la dans environment: (ou dans un seul fichier env final) et imposez l’ordre des fichiers.

Tâche 8 : Détecter les valeurs d’interpolation côté hôte (ce que Compose prélève)

cr0x@server:~$ env | grep -E '^DB_HOST=|^LOG_LEVEL='
LOG_LEVEL=debug

Ce que cela signifie : Votre shell a déjà LOG_LEVEL=debug. Si votre fichier Compose utilise ${LOG_LEVEL}, vous venez d’écraser la config production avec l’historique de votre terminal.

Décision : Lancez Compose avec un environnement propre pour les opérations production, ou définissez explicitement les vars requises dans un fichier contrôlé.

Tâche 9 : Montrer quelles variables manquent lors de l’interpolation (attraper les blancs silencieux)

cr0x@server:~$ docker compose config 2>&1 | grep -i warning
WARNING: The "DB_PASSWORD" variable is not set. Defaulting to a blank string.

Ce que cela signifie : Compose a substitué une chaîne vide. Votre YAML est maintenant valide mais votre système ne l’est pas.

Décision : Traitez cela comme une erreur de déploiement. Configurez la CI pour échouer sur les variables manquantes, ou utilisez des patterns de variables requises dans vos templates.

Tâche 10 : Confirmer les valeurs par défaut de l’image (ENV intégrés à l’image)

cr0x@server:~$ docker image inspect myorg/api:1.8.4 -f '{{json .Config.Env}}'
["PORT=8080","LOG_LEVEL=warn","TZ=UTC"]

Ce que cela signifie : L’image embarque LOG_LEVEL=warn. Si vous pensiez que « unset signifie défaut », l’auteur de l’image a déjà choisi.

Décision : Soit overridez explicitement dans Compose, soit retirez le défaut de l’image s’il cause des surprises (préférez documentation + config explicite).

Tâche 11 : Vérifier si une variable est définie à vide vs non définie (dans le conteneur)

cr0x@server:~$ docker exec api-1 sh -lc 'if printenv OPTIONAL_FLAG >/dev/null 2>&1; then echo "present:[$OPTIONAL_FLAG]"; else echo "unset"; fi'
present:[]

Ce que cela signifie : La variable existe mais est vide. Beaucoup d’apps considèrent cela comme « configuré », et tombent alors dans des chemins de code étranges.

Décision : Si vide doit se comporter comme non défini, ne la définissez pas. Retirez-la de environment: ou de vos fichiers env.

Tâche 12 : Trouver la source d’une valeur en diffant la config rendue et le runtime

cr0x@server:~$ docker compose config --format json | jq -r '.services.api.environment'
{
  "DB_HOST": "db-prod",
  "LOG_LEVEL": "info"
}
cr0x@server:~$ docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api-1 | grep -E 'DB_HOST|LOG_LEVEL'
DB_HOST=db-prod
LOG_LEVEL=info

Ce que cela signifie : Compose et le runtime sont d’accord. Si le comportement est encore erroné, l’appli écrase la config ou la parse mal.

Décision : Orientez l’enquête vers l’application : logs de démarrage, endpoints de dump de config, priorité des librairies.

Tâche 13 : Confirmer ce que Compose pense avoir changé (éviter les déploys non effectifs)

cr0x@server:~$ docker compose up -d
[+] Running 0/0

Ce que cela signifie : Compose n’a rien vu à faire. Votre changement d’env n’est peut-être pas dans la définition du service (ou il n’a pas modifié la sortie de l’interpolation).

Décision : Utilisez --force-recreate ou recréez explicitement le service après avoir confirmé que la config rendue a bien changé.

Tâche 14 : Repérer une héritage involontaire de proxy (classique piège d’entreprise)

cr0x@server:~$ docker exec api-1 sh -lc 'env | grep -i proxy'
HTTP_PROXY=http://proxy.corp:3128
NO_PROXY=localhost,127.0.0.1,db

Ce que cela signifie : Le conteneur utilise un proxy. Ça peut casser les appels service-à-service, la validation des certificats, et ajouter de la latence.

Décision : Si cela ne devrait pas être présent, unsettez ou overridez explicitement les vars proxy dans Compose pour les workloads production.

Mode opératoire de diagnostic rapide

Ceci est l’ordre qui minimise le temps pour atteindre la vérité quand un conteneur est « mal configuré ». N’improvisez pas. Utilisez l’entonnoir.

Première étape : identifier la vérité au runtime

  1. Confirmer le conteneur cible : docker ps et vérifier nom/image/uptime.
  2. Inspecter l’env effectif : docker inspect ... .Config.Env.
  3. Vérifier l’env du PID 1 : docker exec ... /proc/1/environ.

Si l’env runtime est erroné, vous êtes dans le domaine Docker/Compose. Si l’env runtime est correct, vous êtes dans le domaine de l’application.

Deuxième étape : confirmer l’intention rendue par Compose

  1. Rendre la config : docker compose config.
  2. Vérifier les inputs d’interpolation : votre env shell et votre .env projet.
  3. Vérifier le layering : fichiers compose multiples, ordre des env_file, overrides environment.

Troisième étape : confirmer que le changement a bien été déployé

  1. Heure de création : docker inspect ... .Created.
  2. Recréer si nécessaire : docker compose up -d --force-recreate pour le service.
  3. Vérifier à nouveau : inspecter l’env après recréation.

Blague #2 : Dans Docker, la seule configuration cohérente est celle que vous n’aviez pas l’intention d’écraser.

Trois mini-récits d’entreprise (anonymisés, techniquement réels)

Incident : la mauvaise hypothèse (« Compose env écrase tout, non ? »)

Une entreprise de taille moyenne faisait tourner une API de paiements en Docker Compose sur quelques VM. Leur workflow était propre sur le papier : un docker-compose.yml de base plus un fichier d’override par environnement. L’application acceptait la config via des env vars. Classique.

Un déploiement du vendredi a introduit une nouvelle variable PAYMENTS_PROVIDER_TIMEOUT_MS. L’ingénieur l’a définie dans environment: pour l’override production. Le service continuait de timeout sous charge — puis a commencé à relancer agressivement, entraînant des limites de débit en amont.

L’équipe a supposé que la valeur « ne s’appliquait pas ». Ils l’ont augmentée encore. Même comportement. Le vrai problème était que l’image contenait déjà ENV PAYMENTS_PROVIDER_TIMEOUT_MS=2000 et que l’application avait un fichier de config intégré à l’image qui prenait la priorité sur les env vars sauf si USE_ENV_CONFIG=true était défini. En staging, ce flag était défini via un export shell d’un développeur et interpolé dans le fichier d’override. En production, il ne l’était pas. Compose a substitué une valeur vide. Le conteneur a démarré avec le comportement par défaut : ignorer la config via env.

Ils ont débogué le provider, le réseau et la base avant que quelqu’un n’exécute docker compose config et voie l’avertissement sur la variable manquante. La correction fut ennuyeuse : rendre USE_ENV_CONFIG explicite dans la définition du service Compose, et faire échouer le déploiement s’il manque. Puis reconstruire l’image pour cesser d’expédier un fichier de config ambigu.

Conclusion post-mortem : ne considérez pas « la variable existe » comme synonyme de « l’appli l’utilise ». Ce sont deux contrats différents, et l’un d’eux n’avait jamais été écrit.

Optimisation qui a mal tourné : « Dédupliquer la config avec un fichier env partagé »

Une grande équipe d’entreprise en eut assez de répéter la configuration sur 20 services. Ils ont introduit un env/common.env partagé et l’ont inclus via env_file: partout. Puis ils ont ajouté env/prod.env, env/stage.env, etc. Le bruit dans les diffs a diminué. Les gens ont applaudi. C’est comme ça que ça commence.

Trois mois plus tard, un nouveau service a été ajouté. Quelqu’un a copié un stanza de service existant et a oublié d’inclure env/prod.env. Le service est monté avec les valeurs par défaut de l’image et de common.env. Ça « marchait », mais il parlait au cluster cache de staging parce que CACHE_HOST vivait dans prod.env pour la plupart des services et dans common.env pour un service legacy. Les deux valeurs étaient des hostnames plausibles. Pas de crash immédiat. Juste de mauvais chemins de données.

Ils ont essayé de « corriger » en déplaçant plus dans common.env. Cela a élargi le rayon d’impact. Maintenant une inclusion ou omission erronée changeait le comportement à travers plusieurs environnements. Ils ont introduit une « dérive d’environnement par inclusion de fichier ». C’est un type d’échec spécifique : pas une faute de frappe, mais une couche manquante.

La récupération a été d’arrêter de prétendre qu’un fichier env pouvait servir tous les services. Ils ont conservé un fichier partagé, mais limité aux valeurs globales non risquées (timezone, format de log, toggles communs), et exigé que chaque service ait un fichier explicite spécifique à l’environnement. La CI validait que chaque service avait les sources d’env attendues. La déduplication est restée, mais avec des garde-fous.

Ennuyeux mais correct : pinner, rendre, vérifier (et ça a sauvé la mise)

Une petite équipe faisait tourner une plateforme de support client où l’indisponibilité était visible en quelques minutes. Ils avaient une habitude qui semblait paranoïaque : chaque déploiement produisait un artefact contenant la config Compose entièrement rendue (docker compose config) et la liste finale des env vars par service, et ils stockaient cela avec les métadonnées de build.

Un après-midi, l’API a commencé à rejeter des requêtes parce qu’elle se croyait en « mode maintenance ». Ce mode était contrôlé par MAINTENANCE=true. Personne ne l’avait défini. Personne n’a admis l’avoir fait. Slack a fait son œuvre.

Ils ont comparé l’artefact courant au précédent. La config rendue montrait clairement que MAINTENANCE=true avait été interpolé depuis l’environnement hôte lors d’un déploiement de correction manuel. L’ingénieur l’avait exporté plus tôt pour un test local et avait oublié. Compose l’a substitué dans le YAML qui utilisait ${MAINTENANCE} par commodité.

La correction a pris cinq minutes : relancer le déploiement avec un environnement propre et supprimer l’interpolation hôte pour ce flag. La leçon est restée parce qu’elle était mesurable : leur pratique « paranoïaque » a transformé un mystère vague en une seule ligne de diff. Rien de héroïque. Juste des preuves.

Erreurs courantes : symptômes → cause racine → correction

1) Symptomatique : la variable est correcte dans docker-compose.yml, mais le conteneur a une valeur différente

Cause racine : Vous avez changé le YAML mais n’avez pas recréé le conteneur, ou vous regardez un autre projet Compose.

Correction : Vérifiez docker compose ls, confirmez l’heure de création du conteneur, puis docker compose up -d --force-recreate api.

2) Symptomatique : ${VAR} devient vide même si vous l’avez défini sous environment:

Cause racine : Confusion entre interpolation côté hôte et environnement runtime du conteneur. Compose interpole depuis le shell et le .env, pas depuis environment:.

Correction : Déplacez la valeur dans l’environnement hôte (contrôlé), ou cessez d’interpoler et utilisez des littéraux. Validez avec docker compose config.

3) Symptomatique : la valeur diffère entre staging et prod malgré les mêmes fichiers Compose

Cause racine : Environnement shell différent au moment du déploiement (agent CI vs shell humain), ou fichier .env différent dans le répertoire de travail.

Correction : Déployez depuis un environnement contrôlé. Évitez de compter sur des exports shell de développeurs. Stockez les vars d’interpolation requises dans un fichier explicite utilisé par le job de déploiement.

4) Symptomatique : l’appli se comporte comme si un réglage était « activé » même quand vous mettez false

Cause racine : L’appli parse mal les booléens (« chaîne non vide = truthy »), ou utilise un autre nom de variable que vous pensez.

Correction : Confirmez en dumpant la config effective au démarrage de l’appli. Utilisez un parsing strict dans l’appli. Préférez 0/1 si l’appli est laxiste.

5) Symptomatique : des secrets apparaissent dans les logs ou les bundles de support

Cause racine : Les secrets passés via env vars sont faciles à dump via env, endpoints de debug, ou crash dumps.

Correction : Utilisez les secrets Docker (ou des secrets montés en fichier) et passez des chemins, pas des valeurs. Au minimum, masquez et verrouillez les diagnostics.

6) Symptomatique : les requêtes passent aléatoirement par un proxy ou échouent seulement sur certaines machines

Cause racine : Vars proxy héritées depuis l’hôte, une unité systemd, ou le runner CI.

Correction : Réglez/unsettez explicitement HTTP_PROXY/NO_PROXY dans Compose pour la production. Vérifiez à l’intérieur du conteneur.

7) Symptomatique : Compose avertit « variable not set, defaulting to blank string », mais le déploiement réussit quand même

Cause racine : Inputs d’interpolation manquants, et votre pipeline ne traite pas les warnings comme des échecs.

Correction : Gatez les déploiements sur « pas de variables manquantes » en parsant la sortie de docker compose config dans la CI, ou en validant une liste de vars requises avant d’exécuter Compose.

8) Symptomatique : une variable depuis env_file ne semble pas s’appliquer

Cause racine : Elle est écrasée par environment:, par un env_file ultérieur, ou par docker compose run -e.

Correction : Rendez la config, vérifiez l’ordre, et décidez quelle couche est souveraine. Rendez-le explicite.

Listes de contrôle / plan étape par étape

Checklist : rendre la priorité des env vars ennuyeuse (l’objectif)

  1. Arrêtez d’utiliser l’interpolation hôte pour les réglages runtime sauf si vous avez un environnement de déploiement contrôlé. Si cela change, cela doit changer dans une config versionnée.
  2. Privilégiez environment: explicite pour les valeurs critiques (endpoints DB, flags fonctionnels pouvant rompre la sécurité, modes comme maintenance, etc.).
  3. Utilisez env_file: pour des valeurs par défaut en masse mais gardez-le petit et prévisible. Évitez les fichiers « tout-en-un » partagés par tous les services.
  4. Utilisez un seul fichier env spécifique à l’environnement par service si vous devez employer des fichiers env. Le layering est acceptable, mais il exige discipline et validation.
  5. Ne mettez pas de defaults significatifs dans l’image sauf si vous l’assumez. Les defaults d’image sont invisibles aux lecteurs Compose et aiment surprendre.
  6. Rendez les variables manquantes fatales. Une chaîne vide est rarement un défaut sûr pour des credentials, endpoints ou toggles fonctionnels.
  7. Recrééz les conteneurs quand les env changent. Intégrez cela dans votre procédure de déploiement ; ne vous fiez pas à la mémoire humaine.
  8. Enregistrez la config Compose rendue pour chaque déploiement afin de pouvoir diff ce que vous vouliez contre ce que vous avez expédié.
  9. Gardez les secrets hors des env vars. Utilisez des fichiers / secrets, passez des chemins, et auditez ce que vos diagnostics dumpent.
  10. Ajoutez une ligne de log de démarrage ou un endpoint qui imprime la config non secrète (sanitized) pour confirmer l’interprétation de l’app.

Étape par étape : quand vous devez changer une valeur en toute sécurité

  1. Changez la couche autoritaire (généralement environment: ou un seul fichier env final).
  2. Rendez le résultat : exécutez docker compose config et confirmez que la valeur apparaît là où vous l’attendez.
  3. Envoyez le changement : docker compose up -d --force-recreate service.
  4. Vérifiez au runtime : docker inspect et /proc/1/environ.
  5. Vérifiez l’interprétation par l’application : lisez les logs de démarrage ou atteignez un endpoint de status/config.
  6. Notez la règle de priorité pour ce réglage (même une phrase) pour éviter la répétition de l’incident.

FAQ

1) Est-ce que .env devient automatiquement des variables d’environnement du conteneur ?

Non. .env est principalement utilisé par Compose pour la substitution de variables dans le fichier Compose. Pour injecter des valeurs dans le conteneur, utilisez env_file: ou environment:.

2) Qui gagne : env_file ou environment ?

environment gagne. Si les deux définissent FOO, la valeur dans environment: est celle que le conteneur reçoit.

3) Qui gagne : ENV de l’image ou environment de Compose ?

environment de Compose gagne. Les defaults d’image sont la couche de base ; la configuration runtime les écrase.

4) Pourquoi ${VAR} se substitue parfois en chaîne vide sans échouer ?

Parce que Compose traite les variables d’interpolation manquantes comme « non définies » et peut par défaut les remplacer par une chaîne vide, émettant souvent un avertissement. Si vous ne traitez pas les warnings comme des échecs, vous venez d’expédier une valeur vide.

5) J’ai défini une variable et redémarré le conteneur. Pourquoi elle ne s’applique pas ?

Un redémarrage ne change pas la configuration du conteneur. Vous devez recréer le conteneur pour que Docker enregistre la nouvelle env dans la config du conteneur.

6) Est-ce que docker exec env suffit pour savoir ce qui est configuré ?

C’est bien, mais vérifiez l’environnement du PID 1 (/proc/1/environ) si vous suspectez des scripts d’entrypoint ou des supervisors. Confirmez aussi avec docker inspect pour voir ce que Docker pense être la config.

7) Les variables d’environnement sont-elles sûres pour les secrets ?

Ce sont des outils pratiques, pas sûrs. Elles peuvent fuir via les listings de processus, les crash dumps, les endpoints de debug et les bundles de support. Préférez des secrets montés en fichiers et passez des chemins via env vars si nécessaire.

8) Pourquoi différentes machines produisent-elles des configs Compose rendues différentes ?

Parce que l’interpolation dépend de l’environnement où Compose s’exécute : variables shell, un .env local, et parfois des répertoires de travail ou wrappers différents. La config rendue est un artefact de build — traitez-la comme telle.

9) Si mon appli lit DATABASE_URL et aussi DB_HOST, que faire ?

Choisissez un contrat et imposez-le. Si vous devez supporter les deux, définissez une priorité stricte dans l’appli et logguez la source qui a remporté la décision au démarrage (sans afficher les secrets).

10) Comment empêcher l’environnement shell des développeurs d’affecter les déploiements production ?

Exécutez les déploiements depuis la CI ou un environnement de déploiement dédié avec un environnement assaini. Évitez l’interpolation ${VAR} pour les réglages critiques runtime sauf si les inputs sont contrôlés et validés.

Conclusion : prochaines étapes pour arrêter l’hémorragie

Si vous avez traité les env vars comme « simples », Docker est probablement en train de vous contredire silencieusement. Le système n’est pas malveillant — juste superposé. Le remède est de rendre ces couches explicites et observables.

  1. Commencez à utiliser docker compose config comme artefact de déploiement à part entière. Si ce n’est pas rendu, ce n’est pas réel.
  2. Faites de la vérification runtime une routine : docker inspect pour la config conteneur, /proc/1/environ pour la vérité du processus.
  3. Réduisez les couches inutiles : moins de fichiers env, moins d’overrides, moins de « defaults magiques » dans les images.
  4. Faites échouer rapidement sur les variables manquantes. Les warnings sur des valeurs vides devraient être traités comme une build cassée.
  5. Sortez les secrets des env vars. Votre futur rapport d’incident vous remerciera.

Quand un système se comporte bizarrement, le chemin le plus rapide n’est rarement une intuition plus profonde. C’est de demander au runtime ce qu’il a fait, puis de rendre difficile pour les humains de faire la mauvaise chose par accident à nouveau.

← Précédent
Codes sonores BIOS : diagnostiquer les défaillances matérielles à l’oreille (et la panique)
Suivant →
Ubuntu 24.04 : limites d’upload PHP — corriger upload_max_filesize là où ça compte (cas n°10)

Laisser un commentaire