infra / docker-compose
Variables d'environnement et fichiers .env
Explication
Ce que vous allez apprendre
- Distinguer le fichier
.envracine (interpolation du YAML) deenv_file(variables du conteneur) - Savoir laquelle de
environmentouenv_filel'emporte en cas de valeur en double - Fournir une valeur par défaut avec la syntaxe
${VAR:-defaut} - Charger un fichier
.envdifférent selon l'environnement avec--env-file - Ne jamais committer de secrets dans un fichier
.envversionné
Dans quel contexte ?
Un développeur clone le dépôt boutique-api et lance docker compose up, en pensant que la variable DB_PASSWORD définie dans son .env racine sera automatiquement visible dans le code Node.js du conteneur. Elle ne l'est pas : l'application plante avec une erreur "password authentication failed" car cette variable n'a servi qu'à interpoler le YAML, pas à alimenter le conteneur. Cette leçon explique cette distinction essentielle.
Le principe de départ : ne pas coder en dur
D'abord, une règle de base issue des applications "12-factor" : ne jamais coder en dur une valeur qui change selon l'environnement, comme un mot de passe de base de données ou une URL d'API. Ces valeurs doivent vivre en dehors du code, dans des variables d'environnement.
Premier mécanisme : le fichier .env pour l'interpolation
Compose propose un fichier .env à la racine du projet. Son rôle est précis : il remplit les ${VARIABLE} présentes DANS le fichier YAML lui-même, avant même que Compose ne parle à Docker. C'est de la simple substitution de texte dans ta configuration.
Un piège à anticiper tout de suite
Attention, ce .env racine ne finit PAS automatiquement dans le conteneur. Beaucoup de débutants pensent que placer une variable dans ce fichier suffit à la rendre disponible pour l'application — ce n'est pas le cas.
Deuxième mécanisme : env_file et environment pour le conteneur
Pour qu'une variable soit réellement disponible dans le conteneur, via process.env ou équivalent, il faut utiliser env_file ou environment. Ces deux clés injectent véritablement des variables à l'intérieur du conteneur, contrairement au .env racine qui ne fait que de l'interpolation.
Que se passe-t-il en cas de conflit ?
Une fois qu'on utilise les deux mécanismes ensemble, une question se pose naturellement : que se passe-t-il si une variable est définie à la fois par env_file et environment ? La réponse est simple : c'est toujours environment qui gagne, car c'est la déclaration la plus explicite.
| Mécanisme | Où il agit | Visible dans le conteneur ? |
|---|---|---|
.env racine | Interpolation du fichier YAML (${VAR}) | Non, sauf s'il est repris dans environment |
env_file: | Injection de tout un fichier dans le conteneur | Oui |
environment: | Injection directe, clé par clé | Oui, prioritaire sur env_file |
Prérequis
Il est utile d'avoir déjà manipulé des variables d'environnement dans un contexte de développement classique (Node.js, Python...) avant cette leçon, même sans Docker.
Une astuce pratique : les valeurs par défaut
Pour aller plus loin, la syntaxe ${VAR:-valeur_par_défaut} permet de fournir une valeur de secours si la variable n'est pas définie, tout en gardant la possibilité de la surcharger plus tard.
Le piège majeur à ne jamais commettre
Ne committe jamais un .env contenant de vrais secrets dans Git. Il doit systématiquement figurer dans le .gitignore, avec à la place un .env.example versionné qui documente simplement les variables attendues, sans leurs valeurs sensibles.
Piège fréquent
Un .env committé par erreur dans Git reste visible dans l'historique même après suppression du fichier : un git rm .env ne suffit pas à effacer un secret déjà poussé. Ajoute .env au .gitignore dès la création du projet, avant même le premier commit.
La prochaine leçon s'appuie directement sur ces notions pour résoudre un autre problème classique : s'assurer qu'un service ne démarre pas avant qu'un autre soit vraiment prêt.
Commandes & code
Variables d'environnement & .env
services:
api:
image: myorg/api:latest
environment:
# Valeur en dur
NODE_ENV: production
# Interpolée depuis .env ou l'environnement shell, avec valeur par défaut
PORT: "${API_PORT:-3000}"
DATABASE_URL: "postgres://user:${DB_PASSWORD}@db:5432/app"
env_file:
- .env.api # charge toutes les variables du fichier dans le conteneur# .env à la racine du projet : lu automatiquement par "docker compose"
# pour l'INTERPOLATION du fichier YAML (pas injecté au conteneur par défaut)
COMPOSE_PROJECT_NAME=technologik
API_PORT=3000
DB_PASSWORD=s3cr3t_dev_only# .env.api : chargé dans le conteneur via "env_file"
NODE_ENV=production
LOG_LEVEL=info
FEATURE_FLAG_NEW_UI=true# Passer une variable ponctuelle sans toucher au .env
API_PORT=4000 docker compose up -d
# Spécifier un fichier .env différent
docker compose --env-file .env.staging up -d
# Vérifier les valeurs réellement interpolées
docker compose config# Différence env_file vs environment : "environment" a toujours la priorité
services:
api:
env_file: .env.api # LOG_LEVEL=info dans le fichier
environment:
LOG_LEVEL: debug # écrase la valeur du fichier -> debug gagneRésumé
.envracine = interpolation des${VAR}dans le YAML lui-même.env_file:= variables injectées dans le conteneur au runtime.environment:a toujours priorité surenv_file:en cas de conflit.- Ne jamais committer un
.envcontenant de vrais secrets (.gitignore).
Exercices pratiques
Mission : DB_PASSWORD introuvable côté application
Objectif : Diagnostiquer une variable présente dans le .env racine mais absente du conteneur, et résoudre un conflit de priorité.
Contexte
L'application boutique-api plante avec "password authentication failed" alors que DB_PASSWORD=s3cr3t_dev_only est bien défini dans le .env à la racine du projet. Par ailleurs, .env.api contient LOG_LEVEL=info, chargé via env_file, alors que le service définit aussi environment: LOG_LEVEL: debug.
Corrige la configuration et explique les mécanismes en jeu.