Le rechargement à chaud fonctionne jusqu’au moment où vous containerisez l’application, montez votre code, et soudainement votre watcher part en retraite silencieuse. Vous enregistrez un fichier. Rien ne rebuild. Vous enregistrez à nouveau. Toujours rien. Puis vous redémarrez le conteneur et — bien sûr — ça « marche » pendant dix minutes avant de retomber en panne comme un détecteur de fumée capricieux.
Ce n’est pas vous. C’est l’intersection chaotique des API d’événements de système de fichiers, des couches de virtualisation, des partages réseau qui se prennent pour des disques locaux, et des outils de dev qui supposent que l’hôte est « une machine Linux normale ». Rendre la surveillance de fichiers ennuyeuse à nouveau, c’est l’objectif.
Ce qui est réellement cassé (et pourquoi c’est intermittent)
La plupart des systèmes de rechargement à chaud en dev dépendent des notifications d’événements de fichiers au niveau noyau :
- Linux : inotify (généralement via des bibliothèques comme chokidar, watchdog, ou fsnotify)
- macOS : FSEvents / kqueue
- Windows : ReadDirectoryChangesW
Ces API ont été conçues avec une hypothèse simple : « le processus qui observe les événements s’exécute sur la même machine que celle qui possède le système de fichiers. » Le développement Docker casse cette hypothèse de plusieurs façons :
- Les bind mounts traversent des frontières. Votre conteneur peut être Linux, mais vos fichiers réels résident sur APFS macOS, NTFS Windows, VHDX de WSL2, ou un partage réseau.
- Le transfert d’événements est imparfait. Les événements de fichiers peuvent ne pas se propager correctement à travers les couches de virtualisation, et même lorsqu’ils le font, ils peuvent être retardés, coalescés ou perdus.
- Les watchers coûtent cher. Beaucoup d’outils surveillent des arbres de répertoires énormes et atteignent les limites d’inotify, de descripteurs de fichiers ou de CPU. Dans les conteneurs, ces limites peuvent être plus basses ou plus faciles à atteindre.
- Les éditeurs sont « créatifs ». Certains éditeurs écrivent les fichiers via un renommage atomique (écriture d’un fichier temporaire, puis rename), ce qui change le motif d’événements. Certains watchers gèrent bien cela. D’autres non.
Si vous retenez une chose : la surveillance de fichiers n’est pas une unique « fonctionnalité ». C’est une chaîne. Le moindre maillon faible — système de fichiers, driver de montage, couche de virtualisation, limites du noyau, implémentation du watcher — rend le hot reload instable. Votre tâche est d’identifier le maillon le plus faible et d’arrêter de prétendre qu’il va se réparer tout seul.
Blague #1 : Les file watchers sont comme des tout-petits : si vous arrêtez de les surveiller 30 secondes, ils feront quelque chose d’alarmant et refuseront d’expliquer pourquoi.
À quoi ressemble le « cassé » en pratique
Ces modes de défaillance reviennent souvent :
- Pas d’événements du tout. Votre outil ne rebuild jamais sauf si vous le redémarrez.
- Les événements arrivent en retard. Vous enregistrez un fichier et le rebuild se produit 5–30 secondes plus tard, parfois en lots.
- Surveillance partielle. Certains répertoires déclenchent des rebuilds, d’autres jamais.
- CPU élevé avec polling. Vous l’avez « réglé » avec du polling et maintenant le ventilateur de votre laptop auditionne pour un drone.
- Fonctionne sur hôte Linux, échoue sur Docker Desktop. Classique surprise macOS/Windows avec bind mount.
Feuille de diagnostic rapide
Quand le hot reload échoue, ne commencez pas par changer trois paramètres en espérant le meilleur. Commencez par répondre rapidement à trois questions :
1) Est-ce que les événements de fichiers atteignent le conteneur ?
Vérifier : les événements inotify à l’intérieur du conteneur avec un outil minimal. Si vous ne pouvez pas observer les événements directement, vous déboguez une rumeur.
Décision :
- Si les événements n’apparaissent pas : c’est un problème de montage/transmission de virtualisation ou vous n’éditez pas réellement le chemin monté.
- Si les événements apparaissent mais que votre outil ne réagit pas : c’est la config de votre watcher ou une limitation propre à l’outil.
2) Atteignez-vous les limites d’inotify/watch ou les limites de descripteurs de fichiers ?
Vérifier : les sysctls d’inotify, les fichiers ouverts, les logs de l’outil.
Décision :
- Si les limites sont basses : augmentez-les sur l’hôte (et parfois dans le conteneur) et réduisez la portée surveillée.
- Si les limites sont correctes : passez à autre chose — n’appliquez pas de changements sysctl par mimétisme.
3) Le chemin monté est-il suffisamment lent pour que votre watcher « abandonne » ?
Vérifier : performance du bind mount ; surveillez CPU et I/O. Sur macOS/Windows, les mounts peuvent être beaucoup plus lents que les systèmes de fichiers Linux natifs.
Décision :
- Si le mount est lent : déplacez les chemins lourds (node_modules, artefacts de build) dans des volumes de conteneur ; envisagez des outils de sync ; ou passez au polling avec des intervalles raisonnables.
- Si le mount est rapide : concentrez-vous sur la configuration de l’outil et la justesse des événements.
Faits intéressants et contexte historique
La surveillance de fichiers en dev conteneurisé semble moderne, mais les problèmes sous-jacents sont plus vieux que la plupart des frameworks frontend. Voici quelques points de contexte qui aident à expliquer les bizarreries d’aujourd’hui :
- inotify est arrivé dans Linux 2.6.13 (2005). Avant cela, beaucoup d’outils utilisaient le balayage périodique. Le polling est ancien, mais il est aussi prévisible.
- inotify n’est pas récursif. Un watcher doit ajouter des watches pour chaque répertoire. Les gros repos peuvent nécessiter des dizaines de milliers de watches.
- Les premiers Docker sur macOS utilisaient osxfs. Il était célèbre pour ses performances et ses bizarreries d’événements. Docker Desktop moderne est passé par gRPC-FUSE et d’autres approches, mais les cas limites subsistent.
- Les patterns de sauvegarde atomique ont changé la donne. Les éditeurs qui font « write temp + rename » peuvent générer des séquences move/unlink/create ; les watchers naïfs interprètent parfois cela comme une suppression et arrêtent de surveiller.
- fs.watch de Node a des bizarreries de plateforme. Beaucoup d’écosystèmes se sont standardisés sur chokidar parce que fs.watch était incohérent entre OS, surtout pour les mounts réseau.
- WSL2 utilise un système de fichiers Linux virtualisé. Accéder aux fichiers Linux depuis des chemins Windows et inversement traverse une frontière de traduction qui affecte à la fois la vitesse et la sémantique des événements.
- Kubernetes a popularisé les patterns de sync sidecar. Les workflows dev ont emprunté ces idées : garder la source sur l’hôte, la synchroniser dans le conteneur, surveiller à l’intérieur du conteneur sur un système de fichiers natif.
- Watchman a été créé parce que les gros arbres cassent les watchers naïfs. Watchman de Meta existe pour la performance et la correction à grande échelle ; c’est un rappel que « simplement surveiller le répertoire » n’est pas trivial.
Tâches pratiques : commandes, sorties attendues et décisions
Ci-dessous des tâches pratiques que vous pouvez lancer. Chaque tâche inclut : une commande, ce que la sortie signifie, et la décision à prendre. Exécutez-les sur l’hôte ou à l’intérieur du conteneur comme indiqué. Elles sont destinées à arrêter les conjectures.
Tâche 1 : Confirmez que vous éditez bien le chemin monté que vous pensez éditer
cr0x@server:~$ docker compose exec app sh -lc 'pwd; ls -la; mount | head'
/app
total 48
drwxr-xr-x 1 root root 4096 Jan 3 10:12 .
drwxr-xr-x 1 root root 4096 Jan 3 10:12 ..
-rw-r--r-- 1 root root 1283 Jan 3 10:10 package.json
...
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...)
Ce que ça signifie : Vous confirmez le répertoire de travail du conteneur et les fichiers présents. Si le fichier que vous éditez sur l’hôte n’est pas ici, vous ne testez pas la surveillance — vous testez votre imagination.
Décision : Si le répertoire ne correspond pas à l’endroit où vous pensez monter, corrigez d’abord le mapping volumes: dans votre Compose.
Tâche 2 : Inspecter les mounts du conteneur et repérer le bind mount
cr0x@server:~$ docker inspect app --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/Users/alex/work/myapp","Destination":"/app","Mode":"rw","RW":true,"Propagation":"rprivate"}]
Ce que ça signifie : Vous pouvez voir s’il s’agit d’un bind mount ou d’un volume nommé. Les bind mounts sont là où surviennent les problèmes de transfert d’événements cross-OS.
Décision : Si vous êtes sur macOS/Windows et que c’est un bind mount, considérez-le comme « possiblement perdant » pour les événements jusqu’à preuve du contraire.
Tâche 3 : Prouvez que les événements inotify sont visibles à l’intérieur du conteneur
cr0x@server:~$ docker compose exec app sh -lc 'apk add --no-cache inotify-tools >/dev/null; inotifywait -m -e modify,create,delete,move /app'
Setting up watches.
Watches established.
Maintenant, éditez un fichier sur l’hôte sous ce mount et regardez la sortie :
cr0x@server:~$ # (after saving /app/src/index.js on the host)
./ MODIFY src/index.js
Ce que ça signifie : Si vous voyez des événements, le noyau à l’intérieur du conteneur reçoit quelque chose qui ressemble à des changements de fichiers.
Décision : Si vous ne voyez rien, arrêtez de blâmer votre app. C’est la couche de montage/propagation d’événements ou vous éditez en dehors de l’arbre monté.
Tâche 4 : Si des événements manquent, comparez avec des changements faits à l’intérieur du conteneur
cr0x@server:~$ docker compose exec app sh -lc 'echo "# test" >> /app/src/index.js'
cr0x@server:~$ # inotifywait output
./ MODIFY src/index.js
Ce que ça signifie : Si les éditions dans le conteneur produisent des événements mais pas les éditions depuis l’hôte, la couche de montage laisse tomber ou ne transfère pas les événements.
Décision : Orientez-vous vers des workflows de sync-into-container ou des watchers en polling sur macOS/Windows.
Tâche 5 : Vérifiez les limites de watch inotify (hôte et conteneur)
cr0x@server:~$ docker compose exec app sh -lc 'cat /proc/sys/fs/inotify/max_user_watches; cat /proc/sys/fs/inotify/max_user_instances'
8192
128
Ce que ça signifie : 8192 watches est souvent trop bas pour des repos JS modernes, des monorepos ou tout projet avec des arbres de dépendances profonds.
Décision : Si votre projet est non trivial, augmentez-le sur l’hôte (et assurez-vous que le conteneur le voit si pertinent). Sur les hôtes Linux, les limites inotify sont des réglages du noyau hôte.
Tâche 6 : Augmenter les limites inotify sur un hôte Linux (temporaire et persistant)
cr0x@server:~$ sudo sysctl -w fs.inotify.max_user_watches=524288
fs.inotify.max_user_watches = 524288
cr0x@server:~$ sudo sh -lc 'printf "fs.inotify.max_user_watches=524288\nfs.inotify.max_user_instances=1024\n" >/etc/sysctl.d/99-inotify.conf && sysctl --system | tail -n 3'
* Applying /etc/sysctl.d/99-inotify.conf ...
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 1024
Ce que ça signifie : Vous avez supprimé un plafond courant qui fait que les watchers échouent silencieusement ou partiellement.
Décision : Si cela corrige le problème, gardez la config persistante ; puis restreignez la portée surveillée pour ne pas regarder l’univers entier.
Tâche 7 : Détecter les erreurs d’ « exhaustion » de watch dans les outils dev typiques
cr0x@server:~$ docker compose logs -f app | egrep -i 'ENOSPC|inotify|watch|too many|EMFILE' || true
Error: ENOSPC: System limit for number of file watchers reached, watch '/app/src'
Ce que ça signifie : ENOSPC dans ce contexte n’est pas « plus d’espace disque ». C’est « plus de descripteurs de watch ». EMFILE signifie « trop de fichiers ouverts ».
Décision : Augmentez les limites et réduisez la portée surveillée ; ne vous contentez pas de basculer vers un polling agressif en pensant avoir résolu le problème.
Tâche 8 : Confirmer les limites d’ouverture de fichiers à l’intérieur du conteneur
cr0x@server:~$ docker compose exec app sh -lc 'ulimit -n; cat /proc/self/limits | grep "Max open files"'
1048576
Max open files 1048576 1048576 files
Ce que ça signifie : Les limites de descripteurs de fichiers ne sont probablement pas votre goulot d’étranglement si elles sont aussi élevées, mais ne supposez rien ; vérifiez.
Décision : Si ulimit -n est bas (1024/4096), augmentez-le via les paramètres Docker/Compose ou votre environnement shell.
Tâche 9 : Mesurer la latence d’un bind mount avec un test fs grossier mais efficace
cr0x@server:~$ docker compose exec app sh -lc 'time sh -c "for i in $(seq 1 2000); do echo $i >> /app/.watchtest; done"'
real 0m3.421s
user 0m0.041s
sys 0m0.734s
Ce que ça signifie : Ce n’est pas un benchmark, c’est un test rapide. Sur des mounts lents, des simples ajouts répétés peuvent être étonnamment coûteux.
Décision : Si c’est lent (secondes pour de petites boucles), votre outil de watch peut être en retard, et le polling peut fondre le CPU. Envisagez des approches de sync ou déplacez les répertoires lourds hors du mount.
Tâche 10 : Séparer « mount du code source » des « dépendances/artefacts de build »
cr0x@server:~$ cat docker-compose.yml | sed -n '1,120p'
services:
app:
volumes:
- ./:/app
- node_modules:/app/node_modules
volumes:
node_modules:
Ce que ça signifie : Vous gardez le code source sur un bind mount (éditable), mais les dépendances vivent dans un volume géré par le conteneur (rapide, cohérent, moins d’événements).
Décision : Si votre tooling surveille node_modules (il ne devrait pas), cela réduit le bruit et la charge d’événements de toute façon.
Tâche 11 : Confirmez que votre watcher ne surveille pas accidentellement des fichiers à ignorer
cr0x@server:~$ docker compose exec app sh -lc 'node -p "process.cwd()"; node -p "require(\"chokidar\").watch(\"/app\", {ignored: [/node_modules/, /dist/] }).getWatched ? \"ok\" : \"unknown\""'
/app
ok
Ce que ça signifie : Beaucoup d’outils de watch peuvent être configurés pour ignorer des chemins lourds. Si vous n’ignorez pas les sorties de build, vous pouvez déclencher des boucles de rebuild et des explosions de watch.
Décision : Ajoutez des ignores explicites pour node_modules, dist, .next, target, build, etc., selon votre stack.
Tâche 12 : Forcer le polling comme expérience contrôlée (pas comme religion permanente)
cr0x@server:~$ docker compose exec app sh -lc 'CHOKIDAR_USEPOLLING=true CHOKIDAR_INTERVAL=250 npm run dev'
> dev
> vite
[vite] hot reload enabled (polling)
Ce que ça signifie : Le polling supprime la dépendance aux événements de type inotify forwardés. Si le polling fonctionne de façon fiable, votre chaîne de propagation d’événements est le maillon faible.
Décision : Si le polling corrige la justesse mais que le CPU monte en flèche, orientez-vous vers un sync-into-container ou un backend de partage de fichiers plus rapide plutôt que de réduire les intervalles à 50ms comme un gremlin du chaos.
Tâche 13 : Vérifier si les changements sont « basés sur des renommages » (sauvegarde atomique) et si votre outil gère ça
cr0x@server:~$ docker compose exec app sh -lc 'inotifywait -m -e close_write,move,create,delete /app/src'
Setting up watches.
Watches established.
./ MOVED_TO index.js
./ MOVED_FROM .index.js.swp
./ CLOSE_WRITE,CLOSE index.js
Ce que ça signifie : Vous pouvez voir des patterns move/rename plutôt que de simples MODIFY. Certains watchers échouent à rattacher les fichiers déplacés s’ils surveillent des fichiers au lieu des répertoires.
Décision : Configurez votre outil pour surveiller des répertoires, pas des fichiers individuels, et assurez-vous qu’il gère les événements de renommage. Ou changez les réglages « safe write » de l’éditeur pour le repo.
Tâche 14 : Identifier si votre projet se trouve sur le « mauvais » système de fichiers sous WSL2
cr0x@server:~$ docker compose exec app sh -lc 'df -T /app | tail -n 1'
/dev/sdb ext4 25151404 8123456 15789012 34% /app
Ce que ça signifie : Sous WSL2, les meilleures performances et le meilleur comportement des événements viennent généralement du fait de garder le code dans le système de fichiers Linux (ext4 dans la VM) plutôt que sur un chemin monté depuis Windows.
Décision : Si vous voyez quelque chose comme drvfs ou un chemin monté depuis Windows, envisagez de déplacer le repo dans le système de fichiers Linux.
Causes racines par plateforme : Linux, macOS, Windows/WSL2
Hôte Linux (Docker Engine natif) : la base « plutôt saine »
Si votre hôte est Linux et que vous utilisez Docker Engine directement, la surveillance de fichiers marche généralement. Quand ça ne marche pas, c’est typiquement l’un de ces cas :
- Limites inotify trop basses pour des gros repos
- Portée de surveillance trop large (surveiller node_modules, répertoires de build, vendor)
- Weirdness overlayfs + bind mount dans certains cas limites
- Outils qui surveillent des fichiers plutôt que des répertoires et manquent les sauvegardes atomiques
La bonne nouvelle : vous pouvez corriger la plupart de ces problèmes avec des sysctls, des ignores, et des patterns de mounts sensés.
macOS (Docker Desktop) : le coût du partage de fichiers
Sur macOS, Docker exécute des conteneurs Linux dans une VM. Votre bind mount traverse APFS vers cette VM via une couche de partage de fichiers. Cette couche tente de traduire les événements de fichiers macOS en quelque chose que les conteneurs Linux peuvent consommer. « Tente » est un joli euphémisme.
Les réalités macOS les plus courantes :
- Les événements peuvent être coalescés ou retardés. Votre outil voit des rafales plutôt que des modifications en temps réel.
- Certaines sortes d’événements ne se traduisent pas bien. Les patterns move/rename peuvent embrouiller les watchers.
- La performance peut être la vraie défaillance. Le watcher fonctionne mais est asphyxié par des opérations métadonnées lentes.
Sur macOS, la réponse « correcte » pour les équipes sérieuses est souvent : gardez le système de fichiers de travail du conteneur natif (volume), et synchronisez le code dedans.
Windows + Docker Desktop : choisissez bien votre champ de bataille
Windows ajoute une couche de traduction supplémentaire : sémantique NTFS, API de notification de fichiers Windows, et la VM Linux de Docker. Si vous ajoutez WSL2 dans le mix, vous pouvez vous retrouver avec une poupée russe de systèmes de fichiers.
Guidance pratique :
- Meilleur cas WSL2 : gardez le repo dans le système de fichiers Linux (pas sur un lecteur Windows monté), lancez Docker/Compose depuis là.
- Pire cas : éditer les fichiers sur Windows, binder dans un conteneur dans une VM Linux, en attendant une parfaite sémantique inotify. Cette voie finit en polling.
Corrections efficaces : du « suffisant » à l’UX dev « production-grade »
Cette section est opinionnée parce que vous essayez de livrer du code, pas d’organiser un colloque sur la théorie des systèmes de fichiers.
Correction niveau 1 : Faire réussir inotify sur Linux (limites + portée)
Si vous êtes sur un hôte Linux et que vous galérez encore, faites ceci dans l’ordre :
- Augmentez les limites inotify (Tâche 5/6).
- Arrêtez de surveiller les poubelles : ignorez
node_modules, les sorties de build, les caches. - Surveillez des répertoires, pas des fichiers individuels, pour survivre aux sauvegardes atomiques.
- Ne montez pas les dépendances depuis l’hôte : utilisez un volume pour
node_modules,vendor, etc.
Si vous faites ces quatre choses, la plupart des conteneurs de dev hébergés sur Linux se comporteront comme des environnements de dev normaux.
Correction niveau 2 : Polling contrôlé (quand le transfert d’événements est peu fiable)
Le polling n’est pas honteux. Le polling est déterministe. Le polling est aussi la raison pour laquelle la batterie de votre laptop dépose une plainte auprès des RH.
Utilisez le polling quand :
- les événements inotify n’apparaissent pas pour les éditions depuis l’hôte (Tâche 4 montre la discordance)
- la plateforme est macOS/Windows et vous avez besoin d’un correctif rapide et fiable aujourd’hui
Rendez le polling acceptable :
- Poll uniquement les répertoires source dont vous avez besoin.
- Utilisez des intervalles raisonnables (200–1000ms selon la taille du repo).
- Désactivez la surveillance des arbres générés volumineux.
Exemples que vous verrez :
- Outils basés sur chokidar :
CHOKIDAR_USEPOLLING=true, parfois avecCHOKIDAR_INTERVAL. - Webpack : watchOptions.poll
- Python watchdog :
WATCHDOG_USE_POLLING=true(selon l’outil) - Rails : passez au file watcher en polling ou ajustez le backend du gem listen
Correction niveau 3 : Synchroniser le code dans un système de fichiers natif du conteneur (l’approche « ennuyeuse mais correcte »)
Si vous voulez un hot reload qui se comporte comme sur Linux, donnez à votre conteneur un système de fichiers Linux à surveiller. Cela signifie :
- Placez l’arbre de travail dans un volume nommé (rapide, natif dans la VM).
- Synchronisez le code source de l’hôte → volume (unidirectionnel ou bidirectionnel) à l’aide d’un outil de sync.
- Exécutez les watchers à l’intérieur du conteneur contre le chemin du volume.
Cela réduit ou élimine les problèmes de transfert d’événements car la watch se fait sur un vrai système de fichiers Linux, pas sur un mount distant qui fait semblant.
Il existe plusieurs façons d’implémenter le sync :
- Sync basé sur un outil (commun dans les équipes sérieuses)
- Fonctionnalités « develop »/watch de Compose selon la version de Docker et le support de votre environnement
- Boucle rsync manuelle si vous avez besoin de quelque chose de simple et contrôlé
Correction niveau 4 : Séparer les responsabilités : builder sur l’hôte, exécuter dans le conteneur
C’est de l’hérésie dans certaines organisations et du bon sens dans d’autres. Si la raison principale de containeriser le dev est « correspondre à l’exécution prod », vous pouvez quand même construire les assets sur l’hôte et monter seulement les sorties de build dans le conteneur, ou proxyfier les requêtes.
Quand ça marche bien :
- Les outils frontend s’exécutent sur l’hôte (événements fichiers natifs rapides), le conteneur sert l’API/backend.
- Ou le backend s’exécute sur l’hôte, et les dépendances comme les bases de données s’exécutent dans des conteneurs.
Quand ça devient un chaos :
- Quand votre équipe a besoin d’une seule commande pour tout démarrer et que vos politiques réseau rendent le localhost cross-talk pénible.
Correction niveau 5 : Réduire la surface surveillée (votre repo est trop grand)
Les monorepos et les gros repos polyglottes ne se contentent pas de « watcher ». Ils demandent une stratégie :
- Ne surveillez que le package sur lequel vous travaillez activement.
- Utilisez les références de projet et les builds incrémentiels.
- Excluez agressivement
.git, les caches, les dépendances vendorisées et les artefacts générés. - Envisagez des services de watcher dédiés (watchman) si votre stack le supporte.
La citation
Idée paraphrasée (attribuée) : Gene Kim a souligné que la fiabilité vient de rendre le travail visible et de réduire les surprises, pas d’actions héroïques au moment T.
Trois mini-récits d’entreprise issus du terrain
Mini-récit #1 : L’incident causé par une mauvaise hypothèse
Une équipe produit de taille moyenne a déployé « des dev containers pour tout le monde » après un trimestre pénible d’onboarding. Ils avaient une stack Compose propre : API, worker, base de données, et un serveur dev frontend. Ça fonctionnait très bien sur les laptops Linux. Sur macOS, quelques développeurs ont commencé à signaler que l’enregistrement d’un fichier ne rebuildait pas, mais seulement « parfois ». Le lead d’équipe a supposé que c’était le framework. Logique.
Ils ont chassé des explications au niveau applicatif pendant une semaine. Ils ont togglé des settings HMR, changé de bundler, figé des dépendances, et même réécrit un bout du script de dev. Le problème persistait, et la confiance chutait. Les nouvelles recrues ont discrètement décidé que la configuration conteneurisée était « fragile » et ont commencé à exécuter les services directement sur leurs machines, anéantissant l’initiative.
La vraie cause était plus simple et embarrassante : une mauvaise hypothèse que les bind mounts sur Docker Desktop se comportent comme sur Linux. Leurs watchers comptaient sur des événements inotify arrivant rapidement et de façon cohérente. Sur macOS, la couche de traduction d’événements coalesçait occasionnellement les événements pendant des rafales d’écritures (surtout quand l’éditeur sauvegardait plusieurs fichiers rapidement).
La correction n’était pas magique. Ils ont validé la défaillance avec inotifywait à l’intérieur du conteneur et ont vu des séquences manquantes. Puis ils ont déplacé l’arbre de travail dans un volume natif du conteneur et ont synchronisé la source. Soudain, le même outil de watch se comportait parfaitement, parce qu’il surveillait un vrai système de fichiers Linux. L’équipe a retenu la leçon : « les mounts cross-OS sont une couche de compatibilité, pas une garantie. »
Mini-récit #2 : L’optimisation qui s’est retournée contre eux
Une équipe entreprise a décidé d’accélérer le dev en montant tout depuis l’hôte : racine du repo, caches de dépendances, sorties de build, même caches de paquets de langage. Ils voulaient que les rebuilds de conteneur soient instantanés et conserver les installations de dépendances entre redémarrages. Sur le papier, c’était un gain de productivité.
En pratique, ça a créé une boucle de feedback. Les watchers observaient les changements dans les sorties de build et les caches, déclenchaient des rebuilds, ce qui modifiait à nouveau les sorties, déclenchant plus de rebuilds. L’usage CPU n’a pas seulement augmenté — il est resté « toujours élevé », ce qui est une manière polie de dire que les laptops sont devenus des radiateurs.
Pire, le système de watch a commencé à manquer de vrais changements source parce qu’il était noyé dans les événements non pertinents. Les développeurs ont rapporté « hot reload peu fiable », mais la cause sous-jacente était la surcharge d’événements et une portée de watch pathologique. L’équipe l’a « résolu » en activant le polling avec un intervalle court. Ça a encore empiré le problème de ventilateur.
La correction ennuyeuse : arrêter de monter les caches et artefacts de build depuis l’hôte, et arrêter de les surveiller. Ils ont déplacé node_modules et les répertoires de build dans des volumes nommés, mis à jour les patterns d’ignore, et rendu la portée de watch explicite. Le système s’est calmé. Le hot reload est redevenu fiable. L’« optimisation » a été annulée parce qu’elle optimisait la mauvaise chose : la vitesse de rebuild du conteneur au détriment de la stabilité d’exécution.
Mini-récit #3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une entreprise du secteur financier (culture de conformité stricte, minimalisme dans les comportements « cowboy ») avait une politique d’environnement dev : chaque repo devait inclure un script de diagnostic qui imprime les hypothèses d’environnement. Ce n’était pas glamour. Ça agaçait certains ingénieurs. Ça les a aussi sauvés à plusieurs reprises.
Quand la surveillance de fichiers a commencé à échouer après une mise à jour de Docker Desktop, l’équipe n’a pas argumenté sur les frameworks. Ils ont lancé le script. Il vérifiait : type de mount, type de système de fichiers, limites inotify, et si les événements inotify apparaissaient lors d’éditions depuis l’hôte vs à l’intérieur du conteneur. C’était essentiellement les Tâches 1–6 emballées dans une commande.
En quelques minutes, ils avaient une conclusion claire : les éditions depuis l’hôte ne produisaient pas d’événements inotify dans le conteneur, tandis que les éditions in-container le faisaient. Cela a ciblé la couche de partage de fichiers, pas le code applicatif. Ils ont activé un flag dans leur setup dev : les utilisateurs macOS basculaient automatiquement vers un workflow sync-into-volume, les utilisateurs Linux restaient sur les bind mounts.
Le résultat était ennuyeux dans le meilleur sens : moins de tickets de support, moins d’arguments « cela marche sur ma machine », et une arborescence de décisions documentée. La pratique n’était pas brillante ; elle était disciplinée. En termes d’opérations, ils ont réduit le temps moyen pour innocenter l’application.
Erreurs fréquentes : symptômes → cause racine → correction
1) Symptom : « Le hot reload ne fonctionne qu’après redémarrage du conteneur »
Cause racine : le watcher a planté ou s’est arrêté silencieusement après avoir atteint des limites de watch (ENOSPC) ou rencontré des patterns de renommage qu’il ne gère pas.
Correction : vérifiez les logs pour ENOSPC/EMFILE (Tâche 7), augmentez les limites inotify (Tâche 6), et configurez le watcher pour surveiller des répertoires + gérer les sauvegardes atomiques (Tâche 13).
2) Symptom : « Pas de rebuild sur macOS/Windows, mais fonctionne sur hôte Linux »
Cause racine : le transfert d’événements d’un bind mount à travers la VM Docker Desktop est peu fiable pour vos patterns de modification.
Correction : confirmez avec inotifywait (Tâche 3/4). Ensuite, soit activez le polling avec des intervalles raisonnables (Tâche 12), soit passez au sync-into-volume.
3) Symptom : « Les rebuilds se produisent en énormes rafales »
Cause racine : coalescence d’événements ou transfert retardé par la couche de partage de fichiers ; parfois aggravé par des éditeurs qui écrivent plusieurs fichiers ou formatent à la sauvegarde.
Correction : réduisez l’arbre surveillé ; évitez de surveiller les sorties de build ; envisagez un sync-into-container. Le polling peut aider si les rafales sont acceptables.
4) Symptom : « Le CPU monte après avoir activé le polling »
Cause racine : intervalle de polling trop court ; arbre surveillé trop large ; scans lents sur des chemins de bind mount lourds.
Correction : augmentez l’intervalle, réduisez la portée, et retirez les chemins lourds des bind mounts (Tâche 10). Préférez le sync dans un volume si vous avez besoin de rebuilds à faible latence.
5) Symptom : « Certains répertoires déclenchent reload, d’autres jamais »
Cause racine : comportement non récursif (inotify), bugs d’outils dans l’ajout de watches, ou exhaustion de watch pendant l’initialisation.
Correction : augmentez les limites de watch et confirmez que le watcher rapporte l’ensemble des watches ; ignorez les répertoires lourds et incluez explicitement ce qui compte.
6) Symptom : « Les changements dans le code monté apparaissent, mais l’outil ne rebuild toujours pas »
Cause racine : l’outil surveille un chemin différent du mount (commun quand le workdir diffère), ou il est configuré pour utiliser un backend de watch différent.
Correction : affichez les chemins surveillés dans la config de l’outil ; confirmez le workdir (Tâche 1) ; lancez un inotifywait minimal contre le même répertoire.
7) Symptom : « Boucle de rebuild : save déclenche rebuild déclenche save déclenche rebuild… »
Cause racine : le watcher inclut le répertoire de sortie ; un formateur ou générateur écrit dans l’arbre surveillé ; le processus de build touche l’arbre source.
Correction : excluez les sorties, déplacez les sorties dans un répertoire séparé non surveillé, et gardez les artefacts de build hors du bind mount si possible.
8) Symptom : « Le montage en volume Docker corrige le problème, mais maintenant je ne peux plus éditer le code »
Cause racine : vous avez déplacé l’arbre de travail dans un volume mais n’avez pas ajouté de mécanisme de sync.
Correction : adoptez un outil de sync ou une boucle rsync scriptée ; éditez sur l’hôte et synchronisez dans le volume, surveillez in-container.
Blague #2 : Le polling est le « avez-vous essayé de l’éteindre et de le rallumer » de la surveillance de fichiers, sauf qu’il ne s’éteint jamais — juste votre batterie.
Listes de contrôle / plan étape par étape
Étape par étape : obtenir un hot reload fiable en moins d’une heure
- Prouvez que le mount est correct (Tâche 1 et Tâche 2). Si le conteneur ne voit pas les fichiers que vous éditez, arrêtez-vous.
- Prouvez l’existence des événements avec
inotifywait(Tâche 3). Éditez depuis l’hôte et observez. - Distinguez transfert d’événements vs comportement de l’outil (Tâche 4). Si les éditions in-container déclenchent des événements mais pas celles depuis l’hôte, ce n’est pas votre framework.
- Vérifiez l’exhaustion de watch (Tâche 5 et Tâche 7). Corrigez limites et portée.
- Retirez les répertoires lourds des bind mounts (Tâche 10). Placez les dépendances dans un volume nommé.
- Ignorez explicitement les chemins inutiles dans la config du watcher (Tâche 11). Ne comptez pas sur les valeurs par défaut.
- Essayez le polling contrôlé (Tâche 12). Si ça marche, vous avez une voie vers la fiabilité aujourd’hui.
- Si vous êtes sur macOS/Windows et que c’est toujours instable : adoptez le sync-into-volume pour l’arbre de travail.
- Documentez la décision pour votre équipe : « Linux utilise inotify ; macOS utilise sync ou polling ; voici pourquoi. »
Checklist : « Rendre les bind mounts moins pénibles »
- Montez uniquement le code source ; gardez dépendances et sorties dans des volumes nommés.
- Utilisez des patterns d’ignore explicites pour les outils de watcher.
- Gardez les arbres surveillés petits et prévisibles.
- Évitez les boucles de watch en séparant entrées et sorties.
- Privilégiez la surveillance de répertoires plutôt que de fichiers.
Checklist : « Quand arrêter de se battre et basculer vers le sync »
- inotifywait montre des événements manquants pour les éditions depuis l’hôte.
- Le polling fonctionne mais brûle le CPU ou est trop lent aux intervalles acceptables.
- La taille du repo nécessite de larges ensembles de watch et vous atteignez fréquemment les limites.
- Votre équipe est multi-OS et vous avez besoin d’un comportement cohérent entre laptops.
FAQ
1) Pourquoi la surveillance de fichiers marche sur la machine Linux de mon collègue mais pas sur macOS ?
Sur Linux, les conteneurs partagent le noyau de l’hôte et inotify se comporte normalement sur les bind mounts. Sur macOS, les changements de fichiers doivent traverser une couche de partage de fichiers VM, et le transfert d’événements peut être retardé ou imparfait.
2) Augmenter fs.inotify.max_user_watches est-ce toujours sans danger ?
C’est généralement sûr sur des machines de dev, mais cela augmente l’utilisation mémoire du noyau pour le suivi des watches. Augmentez-le parce que vous en avez besoin, pas parce qu’un blog aléatoire vous l’a dit. Ensuite, réduisez la portée surveillée de toute façon.
3) Pourquoi je vois ENOSPC alors que j’ai encore de la place disque ?
Parce que ENOSPC est surchargé : dans ce contexte il signifie « plus d’espace pour les descripteurs de watch », pas des blocs disque. Vérifiez les logs (Tâche 7) et les sysctls inotify (Tâche 5/6).
4) Quel est le contournement le plus rapide si j’ai besoin de fiabilité aujourd’hui ?
Activez le polling pour votre watcher (Tâche 12), augmentez l’intervalle jusqu’à ce que le CPU soit raisonnable, et réduisez la portée surveillée. Puis planifiez une approche long terme (sync-into-volume) si vous êtes sur macOS/Windows.
5) Pourquoi surveiller node_modules pose-t-il tant de problèmes ?
Parce que c’est énorme, ça bouge beaucoup, et il y a des arbres de répertoires profonds. Le surveiller consomme des watches inotify et génère du bruit. La plupart des outils n’ont pas besoin que node_modules soit surveillé ; ils ont juste besoin qu’il soit résolu.
6) Les fonctionnalités « watch » de Docker Compose peuvent-elles remplacer le watcher de mon outil ?
Parfois. Elles peuvent synchroniser les changements et déclencher des actions de rebuild/restart, ce qui peut éviter inotify entièrement. Mais c’est un composant additionnel ; validez qu’il s’intègre à votre workflow et qu’il ne masque pas la latence.
7) Pourquoi les sauvegardes atomiques cassent certaines watchers ?
La sauvegarde atomique est souvent « écrire un fichier temporaire puis renommer ». Si un watcher suit un inode de fichier trop littéralement, il peut manquer que le « nouveau » fichier a remplacé l’ancien. Les surveillances de répertoire gèrent mieux cela.
8) Dois-je lancer mon serveur dev sur l’hôte et ne containeriser que les dépendances ?
Si votre douleur principale est la surveillance de fichiers et que votre appli n’exige pas une parité kernel-level, oui, c’est une option pragmatique. Soyez simplement explicite sur ce que vous sacrifiez en termes de « parité prod ».
9) Pourquoi déplacer le repo dans un volume nommé aide-t-il ?
Parce que le conteneur surveille un système de fichiers Linux natif à l’intérieur de la VM/hôte, évitant les sémantiques de partage cross-OS. Vous échangez l’édition directe contre la correction, puis vous récupérez l’édition via un sync.
Conclusion : prochaines étapes à livrer aujourd’hui
Le hot reload dans les conteneurs n’est pas « cassé » d’une seule manière universelle. Il est cassé de façons spécifiques et reproductibles — transfert d’événements, limites de watch, performances, et hypothèses des outils. Traitez-le comme un problème SRE : mesurer d’abord, changer ensuite, documenter enfin.
- Exécutez
inotifywaitdans le conteneur et prouvez si les événements arrivent depuis les éditions hôte. - Si vous atteignez les limites, augmentez les watches inotify et resserrez la portée surveillée.
- Si vous êtes sur macOS/Windows et que les événements sont peu fiables, choisissez : polling contrôlé maintenant, sync-into-volume pour la santé à long terme.
- Scindez les mounts : bind mount du code source, volume pour dépendances et sorties.
- Écrivez un petit script d’équipe qui lance les vérifications clés, pour que ça ne devienne pas un savoir tribal.
Rendez la surveillance de fichiers ennuyeuse. Votre futur vous en remerciera.