Nodefony 10.0.0-beta.5
Framework Node.js fullstack — HTTP & WebSocket, même contexte

Booter Nodefony en Docker / Kubernetes (cloud-native)

stablemis à jour 2026-10-06

📍 Documentation › Guides › Docker & cloud-native

Le modèle — un process au premier plan, et l’orchestrateur au-dessus#

Un conteneur n’est pas une machine : c’est un process, et l’orchestrateur ne connaît que celui-là. Tout le reste — redémarrage, arrêt, sondes de vie, montée en charge — se décide sur ce process unique. D’où le principe qui gouverne toute cette page : Nodefony démarre au premier plan, reçoit les signaux directement, et ne se met jamais en arrière-plan.

C’est aussi pourquoi la mise à l’échelle ne se fait pas dans le conteneur mais au-dessus : un process par pod, et l’orchestrateur en lance autant qu’il faut.

Règle d’or : au premier plan, jamais détaché#

En Docker et Kubernetes, le process Node démarre au premier plan. Jamais en arrière-plan (&), jamais de mise en démon (PM2 a été retiré du framework).

Pourquoi : l’orchestrateur (Docker, k8s, systemd) pilote le cycle de vie du container via ce process foreground :

nodefony production est déjà foreground : le Kernel boote, les serveurs HTTP/WS écoutent (ce sont leurs handles qui gardent le process vivant), aucune daemonisation.

Ce que ça donne, mesuré#

nodefony production --workers 1 démarre au premier plan, serveurs à l’écoute. Un SIGTERM — c’est ce qu’envoient docker stop et l’arrêt d’un pod — déclenche l’arrêt gracieux : serveurs HTTP et HTTPS fermés proprement, sortie en code 0, en quelques centaines de millisecondes. Aucun SIGKILL n’est nécessaire.

Les quatre signaux d’arrêt sont attachés au même endroit (Cli.ts#Cli.handleSignals), et un second signal pendant l’arrêt coupe court plutôt que d’attendre (Cli.ts#Cli.constructor) — c’est ce qui évite un pod bloqué en fin de vie.

Trois régimes de boot, trois objets Docker/k8s#

Mode Commande Type Objet k8s Garde le process vivant via
Serveur nodefony production SERVER Deployment + Service handles des serveurs HTTP/WS
Batch one-shot nodefony <module>:<cmd> (CONSOLE) CONSOLE Job / CronJob — (exécute puis exit 0)
Daemon (worker queue, consumer, agent) commande CONSOLE qui park CONSOLE Deployment (sans Service) un handle explicite (socket/consumer/timer)

⚠️ Daemon CONSOLE : un await new Promise(() => {}) ne suffit PAS à garder Node vivant (une Promise pending n’est pas un handle d’event loop — Node sort dès l’event loop vide). Un vrai daemon tient un handle actif (consumer Kafka/Redis, socket, timer). Vérifié en test.

Topologie : 1 process = 1 pod (défaut) vs cluster#

Logs#

Vers stdout/stderr, collectés par Docker puis par un collecteur centralisé (Grafana Alloy, Fluent Bit, Vector, l’agent Datadog…). C’est la voie recommandée : le collecteur a déjà pris une ligne écrite quand le process meurt, il ne coûte rien aux requêtes, et changer d’outil ne touche pas au code. Ne pas écrire dans des fichiers depuis le container (éphémère).

Les drivers loki et opensearch du Log Backplane savent aussi envoyer les journaux directement depuis l’application : c’est une option, pour une démo ou un serveur sans collecteur. Ce qu’ils apportent surtout, c’est la relecture par la console d’administration — mais aujourd’hui une destination déclarée fait les deux à la fois (détail : syslog.md).

Le Dockerfile — il est GÉNÉRÉ, on ne le recopie pas#

nodefony create app écrit le Dockerfile et le .dockerignore de l’application, doctrine commentée ligne à ligne : multi-stage, installation sans script, nettoyage de la matière cryptographique dans l’étage de construction, USER 1000:1000 numérique, sonde sur /readyz, forme exec du CMD.

Cette page n’en porte donc pas de copie : un exemple recopié ici dériverait du fichier réel sans que rien ne le dise — et c’est exactement ce qui était arrivé, l’exemple de ce guide ayant perdu en route l’utilisateur non-root, la sonde et la forme exec du CMD. La source est le gabarit :

La base de l’image#

L’image part de node:24-alpine : zéro vulnérabilité critique contre 2 sur node:24-slim, 4 hautes contre 14, et 81 Mo de moins — mesuré sur les images FINALES, application comprise. L’objection historique (« musl et les paquets natifs ») est tombée : better-sqlite3 et @node-rs/argon2 publient leurs binaires musl et s’exécutent après une installation --ignore-scripts.

Les candidates, leurs chiffres et les deux lignes FROM à remplacer pour en changer sont en tête du Dockerfile généré, et sur la page publique de l’image (docker/hub-overview.md dans le dépôt).

Deux points qui ne se devinent pas : les deux étages doivent partager la même libc (un binaire natif installé à la construction ne se charge pas sinon, sur un message qui ne parle pas de libc), et distroless n’est pas un remplacement d’une ligne — ni shell, ni node sur le PATH.

Les deux règles qui survivent à tout changement de base#

Configurer l’image — les secrets que la production exige#

Une image Nodefony tourne en NODE_ENV=production, et une application en production refuse de démarrer sans ses secrets : elle s’arrête en code 78 (EX_CONFIG) en nommant la variable qui manque. C’est voulu. Un secret tiré au hasard au démarrage invaliderait tout ce qu’il a signé au redémarrage suivant, et différerait d’un exemplaire à l’autre — une connexion acceptée par un pod serait refusée par son voisin. S’arrêter en le disant vaut mieux qu’une panne intermittente découverte en production.

Variable Ce qu’elle protège Sans elle, en production
NF_CSRF_SECRET le jeton anti-falsification des formulaires démarrage refusé (78)
NF_JWT_KEYSET la signature des jetons de connexion (paire Ed25519, en JSON) démarrage refusé si l’application en émet
NF_TOTP_KEY le secret de double authentification de chaque compte, chiffré en base démarre, double authentification désactivée
NF_WEBHOOK_KEY les secrets de signature des webhooks, chiffrés en base démarre, webhooks désactivés
NF_ADMIN_PASSWORD le compte admin créé au premier démarrage démarre, aucun compte — console inaccessible

Les générer — une fois, et depuis l’image elle-même (aucun outil sur le poste) :

docker run --rm mon-app:1.0 node_modules/.bin/nodefony security:secrets --env > nodefony.env
echo "NF_ADMIN_PASSWORD=…" >> nodefony.env
docker run -p 5151:5151 --env-file nodefony.env mon-app:1.0

--env écrit une ligne CLÉ=valeur par secret, sans guillemets : le format que --env-file lit littéralement. Trois règles, et chacune évite une panne :

En Kubernetes, ces valeurs vont dans un Secret : kubernetes.md.

La liste COMPLÈTE n’est pas recopiée ici — elle dépend des modules de l’application, et une copie se périmerait. Elle est générée par l’application elle-même : .env.example (par section, chaque variable avec son exigence et sa place en production), et npx nodefony env, qui dit ce qui est posé, d’où vient chaque valeur, et sort en 78 s’il manque une variable requise.

Kubernetes — probes & timeouts#

Le framework expose deux sondes, actives par défaut — rien à écrire :

Sonde Chemin Ce qu’elle répond
Vivacité /livez 200 tant que le process sert — y compris pendant l’arrêt : un pod qui vide ses connexions n’est pas un pod mort, le tuer couperait les requêtes en cours
Disponibilité /readyz 200 quand le pod peut recevoir du trafic, 503 dès le début de l’arrêt — l’équilibreur le retire avant que la première connexion tombe

Elles sont montées avant la limitation de débit (http-kernel.ts#HttpKernel.setErrorRenderer) : un kubelet qui sonde toutes les secondes ne doit pas se faire limiter, puis déclarer le pod mort. Les chemins et l’activation se règlent dans la configuration du module http (config.ts, enabled à true par défaut).

livenessProbe:
  { httpGet: { path: /livez, port: 5151 }, initialDelaySeconds: 10 }
readinessProbe: { httpGet: { path: /readyz, port: 5151 }, periodSeconds: 5 }
terminationGracePeriodSeconds: 30 # > durée du graceful shutdown (~mesurée < 1 s)

📖 Lexique#

Terme Ce que c’est
Premier plan (foreground) Le process reste le process principal du conteneur. C’est lui que l’orchestrateur surveille et signale.
Arrêt gracieux Fermer les serveurs, laisser finir les requêtes en cours, puis sortir — au lieu d’être tué. Déclenché par SIGTERM.
Sonde de vivacité (liveness) Ce qui dit « ce pod est-il encore vivant ? ». S’il échoue, le pod est redémarré.
Sonde de disponibilité (readiness) Ce qui dit « ce pod peut-il recevoir du trafic ? ». S’il échoue, il est retiré du service sans être tué.
Forme exec CMD ["a", "b"] plutôt que CMD a b : sans shell intermédiaire, donc les signaux atteignent Node.
Init Un petit process parent (--init, tini) qui récolte les process orphelins et relaie les signaux.

⚠️ Pièges#

🧪 Tests & couverture#

Les chiffres exacts vivent dans la carte de l’aperçu, régénérée en comptant — jamais figés ici.

Type Où Ce qui est prouvé
Unitaires (signaux, CLI) nodefony Cli.test.ts les signaux attachés, le second signal qui coupe court, les codes de sortie
Unitaires (cluster) nodefony ClusterManager.test.ts, ClusterProbeAggregator.test.ts, ClusterRelay.test.ts la supervision des ouvriers, l’agrégation des sondes, le relais entre process
Intégration (CLI réelle) nodefony CliIntegration.test.ts, detachedStart.test.ts le démarrage effectif, et ce que le détachement change

Ce que ces tests ne couvrent pas : le comportement d’un orchestrateur réel. Le contrat « SIGTERM → arrêt propre » est vérifié sur le process ; ce que Kubernetes en fait dépend de vos terminationGracePeriodSeconds et de vos sondes.

🔗 Pour aller plus loin#