Environnement — variables, cascade des .env, et qui gagne
stablenodefony (cœur)mis à jour 2026-07-25
📍 Documentation › Cœur — @nodefony/core › Environnement
Une variable d’environnement qui « ne prend pas » est le bug le plus long à comprendre du métier : rien n’échoue, rien ne s’affiche, la valeur est simplement ignorée et un défaut s’applique en silence. Cette page dit où poser une valeur, qui l’emporte, et surtout comment le vérifier au lieu de le supposer.
🧭 Démarrage rapide#
nodefony env # la cascade, chaque variable, sa valeur EFFECTIVE et sa PROVENANCE
nodefony env --json # le même rapport, pour un script ou un agent
La commande ne démarre rien (cli/env.ts:208) — elle répond même sur une
application qui ne boote plus, ce qui est précisément le moment où on la lance. Elle sort en
78 (EX_CONFIG) si une variable requise manque, pour qu’un script s’arrête là plutôt que de
tenter un démarrage voué à l’échec.
Ce qu’elle montre, et qu’aucune lecture de fichier ne donne :
- la cascade réelle — quels fichiers sont lus, dans quel ordre, lesquels existent ;
- chaque variable déclarée par l’application, sa valeur effective et le fichier qui l’a fournie ;
- ce qui est masqué : une valeur écrite dans un fichier de rang inférieur, donc sans effet ;
- les variables
NF_inconnues — presque toujours une faute de frappe, avec la correction probable ; - les variables posées par le framework lui-même, à part, avec ce que chacune signale.
Pourquoi ces deux dernières listes sont séparées. Le framework écrit des
NF_*dans l’environnement sans que vous les ayez demandées : le lanceur du CLI marque sa délégation aunodefonydu projet (NF_CLI_DELEGATED), les commandes de démarrage inscrivent le mode (NF_MODE_START), le maître de grappe signale la grappe (NF_CLUSTER), la déclaration MCP porte son jeton (NF_MCP_TOKEN). Rangées parmi les inconnues, elles vous accusaient d’une faute de frappe que vous n’aviez pas commise — et proposaient de « corriger »NF_CLI_DELEGATEDenNF_ADMIN_PASSWORD. Elles ne sont pas tues pour autant : une variable présente que le rapport passe sous silence vous ferait chercher pourquoi votre environnement ne ressemble pas à ce qu’il montre.
Le modèle : deux axes, une cascade, un seul lecteur#
Trois décisions gouvernent tout ce qui suit, et rien d’autre n’est à retenir.
Deux axes plutôt qu’un : comment le code s’exécute (le mode) et où il s’exécute (le
déploiement) sont deux questions distinctes — un staging tourne en mode production.
Une cascade, jamais un écrasement : chaque source pose ce que les plus fortes n’ont pas déjà posé. La précédence n’est donc pas une règle appliquée quelque part, c’est une conséquence de l’ordre de lecture — il n’y a rien à synchroniser, et rien qui puisse diverger.
Un seul lecteur de process.env : env.ts. Tout le reste de l’application lit un objet
typé, validé au démarrage. Une variable non déclarée là n’existe pas, quoi qu’en dise un fichier
.env — c’est ce qui rend une faute de frappe muette, et c’est pourquoi nodefony env existe.
Les deux axes : mode et déploiement#
Nodefony sépare ce que la plupart des frameworks confondent :
| Axe | Variable | Valeurs | Ce qu’il décide |
|---|---|---|---|
| Mode | NODE_ENV |
development / production |
comment le code s’exécute (optimisations, journaux) |
| Déploiement | APP_ENV / NF_ENV |
chaîne libre : staging, canary… |
où il s’exécute (quelle base, quels secrets) |
Un staging tourne en mode production : ce sont deux questions différentes, et les mélanger
oblige à choisir entre « optimisé » et « pointe la bonne base ». Le déploiement est plus
spécifique que le mode, donc plus fort dans la cascade.
Et si NODE_ENV n’est pas posé ?#
C’est le cas de tous les jours sur une machine de développement — et la réponse n’est pas
« au hasard ». Poser NODE_ENV est un acte de déploiement ; ne rien poser est l’état d’un
poste de développement.
NODE_ENV |
Mode retenu | Pourquoi |
|---|---|---|
development / dev |
development |
déclaré |
production / prod |
production |
déclaré |
| absent | development |
personne n’a rien dit → poste de développement |
posé mais autre (staging, canary, prod-eu…) |
production |
un DÉPLOIEMENT est nommé — il tourne comme prod |
| chaîne vide | production |
choix conservateur : « vidée » ≠ « jamais posée » |
Une chaîne vide compte comme posée : on ne distingue pas « vidée par erreur » de « vidée exprès », et se tromper vers la production ne coûte qu’une commande utilitaire, là où l’inverse exposerait la console d’administration d’un serveur.
Le défaut ne gouverne jamais un serveur. nodefony development, nodefony production
(alias start, prod) et nodefony cluster posent leur mode eux-mêmes, et il n’existe pas
d’autre façon d’en démarrer un. Le défaut ne concerne donc que les commandes utilitaires —
inspect, check, env, security:*.
⚠️ Le piège à connaître : une commande utilitaire ne tourne PAS dans le mode du serveur que vous avez lancé. Chacune démarre son propre noyau. Si votre serveur tourne par
nodefony developmentmais queNODE_ENVn’est pas dans votre shell, une commande lancée à côté partira bien endevelopment— mais le jour où vous exportezNODE_ENV=productionpour un essai, elle changera de base de données sans rien dire d’autre. Demandez le mode plutôt que de le supposer :npx nodefony env # le mode, et d'où vient chaque variable npx nodefony inspect routes # la dernière ligne indique l'environnementPour forcer explicitement, préfixez la commande :
NODE_ENV=production npx nodefony ….
La cascade — qui gagne#
Du plus fort au plus faible. Le premier niveau qui pose une valeur gagne ; les suivants sont ignorés, sans message.
| Rang | Source | Committé ? | Rôle |
|---|---|---|---|
| 1 | process.env |
— | shell, orchestrateur, k8s — gagne toujours |
| 2 | .env.<déploiement>.local |
❌ non | secrets de CE déploiement, sur CETTE machine |
| 3 | .env.<mode>.local |
❌ non | secrets du mode |
| 4 | .env.local |
❌ non | secrets communs, machine du développeur |
| 5 | .env.<déploiement> |
✅ oui | réglages partagés du déploiement |
| 6 | .env.<mode> |
✅ oui | réglages partagés du mode |
| 7 | .env |
✅ oui | défauts communs — le plus faible |
Deux règles suffisent à retrouver cet ordre de mémoire : les *.local priment sur les
committés, et à rang égal, le plus spécifique gagne.
L’ordre est produit par une fonction unique, envFileOrder
(loadEnv.ts:72) — celle-là même que nodefony env affiche : un
ordre montré qui différerait de l’ordre appliqué serait pire que pas d’affichage du tout.
L’injection (loadEnv.ts:88) n’écrase jamais une clé déjà
posée, ce dont toute la précédence découle.
Le chargement a lieu une fois, au démarrage du binaire, avant la construction du noyau :
les configurations de modules lisent process.env pendant le boot, il doit donc être peuplé
avant elles.
Déclarer une variable — env.ts#
env.ts est le seul endroit du projet qui lit process.env
(defineEnv.ts:270). Une variable non déclarée là n’existe pas
pour l’application, quoi qu’un fichier .env en dise.
import { defineEnv, envNumber, envEnum, envString } from "nodefony";
export const env = defineEnv({
NF_PORT: envNumber({ default: 5151, description: "Port HTTP." }),
NF_LOG_DRIVER: envEnum(["stdout", "file", "null"], {
default: "stdout",
description: "Destination des journaux.",
}),
NF_DATABASE_URL: envString({
optional: true,
description: "URL de la base.",
}),
});
Ce que la déclaration apporte, et qu’une lecture directe de process.env ne donne pas : la
valeur est typée (number, boolean, énumération), validée au démarrage (une valeur
hors énumération échoue tout de suite, avec le nom de la variable), documentée
(description alimente .env.example et nodefony env), et atteignable typée dans la
configuration via ctx.env.
Une variable requise est celle qui n’a ni défaut ni optional: true. nodefony env les
nomme, et sort en erreur si l’une manque.
Requise LÀ-BAS seulement — requiredIn#
Certaines variables ne sont indispensables qu’en production. Les déclarer optional est vrai
sur le poste du développeur et faux là où ça compte ; les déclarer requises empêcherait de
démarrer en local. requiredIn nomme les environnements où l’absence devient une faute :
NF_CSRF_SECRET: envString({
optional: true,
requiredIn: ["production"],
description: "Secret des jetons anti-CSRF — partagé entre process en cluster.",
}),
Le cas qui fonde cette règle n’est pas un secret laissé en dur, c’est l’inverse : un secret absent est engendré à la volée. Rien ne va mal au premier démarrage — c’est au deuxième exemplaire que les jetons émis par l’un se font refuser par l’autre, sans une ligne dans les journaux.
Les noms sont libres : ils se comparent aux étiquettes de l’environnement courant, à savoir
le mode d’exécution (NODE_ENV) et l’environnement de déploiement (NF_ENV, ou l’alias de
plateforme APP_ENV) quand il en diffère. Une préproduction qui tourne en production porte
donc les deux, et requiredIn: ["preprod"] y mord.
Trois lecteurs appliquent la MÊME règle
(isEnvVarRequired) : le démarrage refuse de partir, nodefony env marque la variable, et nodefony doctor la nomme avant qu’on déploie.
Demander ce qui manquera ailleurs — --env#
nodefony doctor --env production et nodefony env --env production évaluent les exigences
pour l’environnement visé, avec les valeurs présentes ici : on ne simule pas un
déploiement, on demande ce qui manquera là-bas. Le rapport l’annonce en tête, et sort en erreur
si une variable requise à destination n’a aucune valeur.
doctor signale aussi un fichier .env*.local suivi par git — l’historique garde les
secrets même après suppression. Sans dépôt git, il ne conclut pas : il énonce le contrôle comme
non fait.
Secrets : <VARIABLE>_FILE#
Un secret monté par Docker ou Kubernetes est un fichier, pas une valeur. Toute variable
accepte donc la forme <VARIABLE>_FILE, qui pointe le fichier à lire :
NF_TOTP_KEY_FILE=/run/secrets/totp_key # au lieu de NF_TOTP_KEY=…
Poser les deux échoue au démarrage : entre deux sources contradictoires, deviner serait le pire
service. Les valeurs des variables dont le nom évoque un secret ne sont jamais rendues en clair
par nodefony env — seulement leur présence, leur longueur et leur provenance.
Surcharger une clé de module — NF__#
Deux mécanismes coexistent, et les confondre est l’erreur la plus fréquente :
| Forme | Ce que c’est | Où c’est déclaré |
|---|---|---|
NF_PORT=5151 |
variable de l’application, typée et validée | env.ts — non déclarée = sans effet |
NF__HTTP__SERVERS__HTTPS__PORT=8443 |
surcharge directe d’une clé de module | rien à déclarer — __ sépare les segments |
Le second (envOverride.ts:80) vise une clé de configuration
par son chemin, sans passer par env.ts : NF__<MODULE>__<CHEMIN…>. Une liste s’écrit en
valeurs séparées par des virgules. Un segment mal orthographié est signalé au démarrage avec la
clé la plus proche — le « vouliez-vous dire » de git.
Réserve ce mécanisme à ce qu’il fait bien : régler une brique en exploitation sans toucher au
code. Ce que l’application possède en propre se déclare dans env.ts.
🧪 Tests#
Le calcul du rapport est un module pur (envReport.ts:177) : il
reçoit la cascade déjà lue et l’environnement effectif, et conclut. Cette séparation est ce qui
rend éprouvables les trois affirmations sur lesquelles on va se fier pour corriger une
configuration — d’où vient une valeur, ce qui est masqué, ce qui n’a aucun effet. Se tromper sur
l’une d’elles est pire que de ne rien afficher : on croit le rapport, et on cherche ailleurs.
⚠️ Pièges#
- La valeur est dans le fichier, et n’a aucun effet. Elle est posée à un rang inférieur à
celui qui la définit déjà — typiquement
.envalors que.env.localla porte.nodefony envl’affiche comme ignorée dans …, avec le fichier gagnant. - Le shell gagne toujours. Une variable exportée dans le terminal (ou par l’orchestrateur) ne peut être contredite par aucun fichier. C’est voulu : en production, l’orchestrateur fait autorité.
- Une faute de frappe est silencieuse.
NF_PROTau lieu deNF_PORTn’échoue pas : la variable est inconnue, donc ignorée, et le défaut s’applique. Aucun démarrage ne le dira —nodefony envest le seul endroit qui la montre. - Un
.env.localn’est jamais committé. C’est la règle qui rend les secrets tenables ; le.gitignoregénéré l’applique dès la création de l’application. Un secret dans.envpart dans le dépôt. - Le catalogue exige un build.
nodefony envlit les variables déclarées dans ledist/de l’application. Sans build, la cascade reste exacte et le rapport dit que la liste manque — il ne se tait pas.
📖 Lexique#
- Cascade — la suite ordonnée des sources d’environnement, du shell au
.envcommun. - Mode (
NODE_ENV) — comment le code s’exécute :developmentouproduction. - Déploiement (
APP_ENV) — où il s’exécute :staging,canary,prod-eu… chaîne libre. - Masquée — variable définie dans un fichier, mais fournie par une source plus forte : elle est ignorée.
- Effective — la valeur que l’application verra réellement, après application de la cascade.
- Catalogue — l’ensemble des variables qu’une application déclare dans
env.ts, avec leur type, leur défaut et leur description.
🔗 Pour aller plus loin#
- ⬆️ Retour au hub : Cœur — @nodefony/core
- ⚙️ Configurer les modules (le
use()du manifeste, les schémas Zod) : guide de configuration - 🧩 Quel module installer : catalogue des modules
- 🖥️ Les autres commandes : CLI · le cycle de vie qui consomme cet environnement : kernel