La recarga en caliente funciona hasta que contenedorizas la aplicación, montas tu código y, de repente, tu watcher se va de retiro silencioso. Guardas un archivo. Nada se recompila. Guardas otra vez. Aún nada. Entonces reinicias el contenedor y—por supuesto—“funciona” durante diez minutos y vuelve a fallar como un detector de humo defectuoso.
Esto no eres tú. Es la intersección desordenada de APIs de eventos de sistema de ficheros, capas de virtualización, recursos compartidos de red que se hacen pasar por discos locales y herramientas de desarrollo que asumen que el host es “una caja Linux normal”. Hagamos que la vigilancia de archivos vuelva a ser aburrida.
Qué está realmente roto (y por qué es inconsistente)
La mayoría de los sistemas de recarga en caliente dependen de notificaciones de eventos de ficheros a nivel de kernel:
- Linux: inotify (normalmente a través de librerías como chokidar, watchdog o fsnotify)
- macOS: FSEvents / kqueue
- Windows: ReadDirectoryChangesW
Esas APIs fueron diseñadas con una suposición sencilla: “el proceso que observa los eventos corre en la misma máquina que posee el sistema de ficheros”. El desarrollo con Docker rompe esa suposición de varias formas:
- Los bind mounts cruzan límites. Tu contenedor puede ser Linux, pero tus ficheros reales pueden residir en APFS de macOS, NTFS de Windows, VHDX de WSL2 o en un recurso compartido de red.
- El reenvío de eventos es imperfecto. Los eventos de ficheros pueden no propagarse correctamente a través de capas de virtualización y, aun cuando lo hacen, pueden llegar retrasados, coalescidos o perdidos.
- Vigilar no es barato. Muchas herramientas vigilan árboles de directorios enormes y alcanzan los límites de inotify, límites de descriptores de archivo o restricciones de CPU. Dentro de contenedores, esos límites pueden ser más bajos o más fáciles de alcanzar.
- Los editores son “creativos”. Algunos editores guardan archivos mediante rename atómico (escribir archivo temporal, renombrar), lo que cambia el patrón de eventos. Algunos watchers manejan esto bien. Otros no.
Si te llevas una idea: la vigilancia de archivos no es una “feature” única. Es una tubería. Cualquier eslabón débil—sistema de ficheros, controlador de montaje, capa de virtualización, límites del kernel, implementación del watcher—hace que la recarga en caliente sea inestable. Tu trabajo es identificar el eslabón más débil y dejar de fingir que se arreglará solo.
Broma #1: Los file watchers son como niños pequeños: si dejas de mirarlos 30 segundos, harán algo alarmante y se negarán a explicar por qué.
Cómo se ve lo “roto” en la práctica
Estos modos de fallo aparecen repetidamente:
- No hay eventos en absoluto. Tu herramienta nunca recompila a menos que la reinicies.
- Los eventos llegan tarde. Guardas un archivo y la recompilación ocurre 5–30 segundos después, a veces en lotes.
- Vigilancia parcial. Algunos directorios desencadenan recompilaciones, otros nunca lo hacen.
- Alta CPU con sondeo. “Lo arreglaste” con polling y ahora el ventilador del portátil hace audiciones para un trabajo de dron.
- Funciona en host Linux, falla en Docker Desktop. El clásico susto con bind mounts en macOS/Windows.
Guía rápida de diagnóstico
Cuando la recarga en caliente falla, no empieces cambiando tres ajustes y esperando. Empieza respondiendo tres preguntas rápidamente:
1) ¿Los eventos de archivos llegan al contenedor?
Comprueba: eventos inotify dentro del contenedor usando una herramienta mínima. Si no puedes observar eventos directamente, estás depurando un rumor.
Decisión:
- Si no aparecen eventos: es un problema de montaje/reenvío de virtualización o no estás editando la ruta montada.
- Si aparecen eventos pero tu herramienta no reacciona: es la configuración del watcher o una limitación específica de la herramienta.
2) ¿Estás alcanzando límites de inotify/descriptor de archivo?
Comprueba: sysctls de inotify, archivos abiertos, logs de la herramienta.
Decisión:
- Si los límites son bajos: elévalos en el host (y a veces en el contenedor) y reduce el alcance vigilado.
- Si los límites son adecuados: sigue adelante—no apliques cambios de sysctl por imitación.
3) ¿La ruta montada es lo bastante lenta como para que tu watcher “se rinda”?
Comprueba: rendimiento del bind mount; observa CPU y I/O. En macOS/Windows, los montajes pueden ser drásticamente más lentos que los sistemas de ficheros Linux nativos.
Decisión:
- Si el montaje es lento: mueve rutas críticas (node_modules, artefactos de compilación) a volúmenes de contenedor; considera herramientas de sincronización; o cambia a polling con intervalos sensatos.
- Si el montaje es rápido: céntrate en la configuración de la herramienta y en la corrección de eventos.
Datos históricos e contexto interesante
Vigilar archivos en entornos contenedorizados parece moderno, pero los problemas subyacentes son más antiguos que la mayoría de los frameworks frontend. Aquí hay algunos puntos de contexto que ayudan a explicar las rarezas actuales:
- inotify llegó en Linux 2.6.13 (2005). Antes de eso, muchas herramientas usaban escaneo periódico. El polling es clásico, pero también predecible.
- inotify no es recursivo. Un watcher debe añadir vigilancias para cada directorio. Repos grandes pueden requerir decenas de miles de watches.
- Docker temprano en macOS usaba osxfs. Era notorio por su rendimiento y rarezas en eventos. Docker Desktop moderno pasó por gRPC-FUSE y otros enfoques, pero siguen existiendo casos límite.
- Los patrones de guardado atómico cambiaron el juego. Los editores que “escriben temporal + renombrar” pueden generar secuencias de rename/unlink/create; los watchers ingenuos interpretan eso como eliminación y dejan de vigilar.
- fs.watch de Node tiene particularidades de plataforma. Muchos ecosistemas estandarizaron en chokidar porque fs.watch era inconsistente entre OS, especialmente en montajes en red.
- WSL2 usa un sistema de ficheros Linux virtualizado. Acceder a archivos Linux desde rutas Windows y viceversa cruza una frontera de traducción que afecta tanto velocidad como semántica de eventos.
- Kubernetes popularizó patrones de sincronización sidecar. Flujos de trabajo de desarrollo tomaron esas ideas: mantener el código en el host, sincronizar dentro del contenedor y vigilar dentro del contenedor en un sistema de ficheros nativo.
- Watchman se creó porque árboles grandes rompen watchers ingenuos. Watchman de Meta existe por rendimiento y corrección a escala; recuerda que “solo vigilar el directorio” no es trivial.
Tareas prácticas: comandos, salida esperada y decisiones
A continuación hay tareas prácticas que puedes ejecutar. Cada tarea incluye: un comando, qué significa la salida y la decisión que debes tomar. Ejecútalas en el host o dentro del contenedor según se indica. Están pensadas para eliminar conjeturas.
Tarea 1: Confirma que estás editando la ruta montada que crees
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=...)
Qué significa: Estás confirmando el directorio de trabajo del contenedor y los ficheros presentes. Si el fichero que editas en el host no está aquí, no estás probando la vigilancia—estás probando tu imaginación.
Decisión: Si el directorio no coincide con la ubicación de montaje esperada, arregla el mapeo de volumes: en tu Compose primero.
Tarea 2: Inspecciona los montajes del contenedor y localiza el 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"}]
Qué significa: Puedes ver si es un bind mount o un volumen nombrado. Los bind mounts son donde viven los problemas de reenvío de eventos entre sistemas operativos.
Decisión: Si estás en macOS/Windows y esto es un bind mount, trátalo como “posiblemente con pérdida” para eventos hasta que se demuestre lo contrario.
Tarea 3: Demuestra que los eventos inotify son visibles dentro del contenedor
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.
Ahora edita un archivo en el host bajo ese montaje y observa la salida:
cr0x@server:~$ # (after saving /app/src/index.js on the host)
./ MODIFY src/index.js
Qué significa: Si ves eventos, el kernel dentro del contenedor está recibiendo algo que parece cambios de ficheros.
Decisión: Si no ves nada, deja de culpar a tu app. Esto es propagación de eventos/montaje o estás editando fuera del árbol montado.
Tarea 4: Si faltan eventos, compara con cambios hechos dentro del contenedor
cr0x@server:~$ docker compose exec app sh -lc 'echo "# test" >> /app/src/index.js'
cr0x@server:~$ # inotifywait output
./ MODIFY src/index.js
Qué significa: Si las ediciones dentro del contenedor generan eventos pero las del host no, la capa de montaje está perdiendo o no reenviando eventos.
Decisión: Avanza hacia flujos de trabajo de sincronización al interior del contenedor o watchers por polling en macOS/Windows.
Tarea 5: Comprueba límites de inotify (host y contenedor)
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
Qué significa: 8192 watches suele ser demasiado bajo para repos JS modernos, monorepos o cualquier cosa con árboles dependientes profundos.
Decisión: Si tu proyecto no es trivial, elévalo en el host (y asegúrate de que el contenedor lo vea si es relevante). En hosts Linux, los límites de inotify son ajustes del kernel del host.
Tarea 6: Eleva límites de inotify en un host Linux (temporal y persistente)
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
Qué significa: Has eliminado un techo común que hace que los watchers fallen silenciosa o parcialmente.
Decisión: Si esto lo arregla, mantén la configuración persistente; luego reduce el alcance vigilado para no observar el universo entero.
Tarea 7: Detecta errores de “agotamiento de watches” en herramientas típicas
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'
Qué significa: ENOSPC en este contexto no es “sin espacio en disco”. Es “sin descriptores de watch”. EMFILE es “demasiados archivos abiertos”.
Decisión: Eleva límites y reduce el alcance; no cambies a polling agresivo y lo llames solución.
Tarea 8: Confirma límites de archivos abiertos dentro del contenedor
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
Qué significa: Los límites de descriptores de archivo probablemente no son tu cuello de botella si son tan altos, pero no lo des por hecho; compruébalo.
Decisión: Si ulimit -n es bajo (1024/4096), elévalo mediante ajustes de Docker/Compose o tu entorno de shell.
Tarea 9: Mide la latencia del bind mount con una prueba de fs sencilla pero efectiva
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
Qué significa: Esto no es un benchmark, es una prueba de humo. En montajes lentos, simples appends repetidos pueden ser sorprendentemente caros.
Decisión: Si esto es lento (segundos para bucles pequeños), tu herramienta de vigilancia podría retrasarse, y el polling podría fundir la CPU. Considera enfoques de sincronización o mover directorios pesados fuera del montaje.
Tarea 10: Separa “montaje de código fuente” de “dependencias/artefactos de compilación”
cr0x@server:~$ cat docker-compose.yml | sed -n '1,120p'
services:
app:
volumes:
- ./:/app
- node_modules:/app/node_modules
volumes:
node_modules:
Qué significa: Mantienes el código fuente en un bind mount (editable), pero las dependencias viven en un volumen gestionado por el contenedor (rápido, consistente, menos eventos).
Decisión: Si tu tooling vigila node_modules (no debería), esto reduce ruido y carga de eventos de todas formas.
Tarea 11: Confirma que tu watcher no esté vigilando por accidente basura ignorada
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
Qué significa: Muchas herramientas de vigilancia se pueden configurar para ignorar rutas pesadas. Si no ignoras outputs de compilación, puedes desencadenar bucles de recompilación y explosiones de watchers.
Decisión: Añade ignores explícitos para node_modules, dist, .next, target, build, etc., según tu stack.
Tarea 12: Fuerza polling como experimento controlado (no como religión 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)
Qué significa: El polling elimina la dependencia de eventos tipo inotify reenviados. Si el polling funciona de forma fiable, la propagación de eventos es el eslabón débil.
Decisión: Si el polling arregla la corrección pero la CPU se dispara, avanza hacia sincronización al interior del contenedor o hacia un backend de compartición de ficheros más rápido en lugar de reducir intervalos a 50ms como un gremlin del caos.
Tarea 13: Verifica si los cambios son “basados en rename” (guardado atómico) y si tu herramienta lo maneja
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
Qué significa: Puede que estés viendo patrones de move/rename en lugar de MODIFY simple. Algunos watchers fallan al volver a engancharse a archivos movidos si vigilan archivos en vez de directorios.
Decisión: Configura tu herramienta para vigilar directorios, no archivos individuales, y asegura que maneje eventos de renombrado. O cambia la opción de “safe write” del editor para el repositorio.
Tarea 14: Identifica si tu proyecto está dentro del sistema de ficheros “equivocado” en WSL2
cr0x@server:~$ docker compose exec app sh -lc 'df -T /app | tail -n 1'
/dev/sdb ext4 25151404 8123456 15789012 34% /app
Qué significa: En WSL2, el mejor rendimiento y comportamiento de eventos generalmente proviene de mantener el código en el sistema de ficheros Linux (ext4 en la VM) en lugar de una ruta montada desde Windows.
Decisión: Si ves algo como drvfs o una ruta montada desde Windows, considera mover el repositorio al sistema de ficheros Linux.
Causas raíz por plataforma: Linux, macOS, Windows/WSL2
Host Linux (Docker Engine nativo): la línea base “más sensata”
Si tu host es Linux y usas Docker Engine directamente, la vigilancia de archivos suele funcionar. Cuando no lo hace, típicamente es uno de estos:
- Límites de inotify demasiado bajos para repos grandes
- Alcance de vigilancia demasiado amplio (vigilando node_modules, directorios de build, vendor)
- Rariedades con overlayfs + bind mount en algunos casos límite
- Herramientas que vigilan archivos y no directorios y fallan con guardados atómicos
La buena noticia: puedes arreglar la mayoría con sysctls, ignores y patrones de montaje sensatos.
macOS (Docker Desktop): el impuesto por compartir ficheros
En macOS, Docker ejecuta contenedores Linux en una VM. Tu bind mount atraviesa APFS hacia esa VM mediante una capa de compartición de ficheros. Esa capa trata de traducir eventos de archivos de macOS a algo que los contenedores Linux puedan consumir. “Trata” ya implica mucho trabajo en esa frase.
Las realidades más comunes en macOS:
- Los eventos pueden coalescer o retrasarse. Tu herramienta ve ráfagas en lugar de ediciones en tiempo real.
- Algunos tipos de evento no se traducen bien. Patrones de move/rename pueden confundir a los watchers.
- El rendimiento puede ser el fallo real. El watcher funciona pero está hambriento por operaciones de metadatos lentas.
En macOS, la respuesta “correcta” para equipos serios suele ser: mantiene el sistema de ficheros de trabajo del contenedor nativo (volumen) y sincroniza el código hacia él.
Windows + Docker Desktop: elige bien tu campo de batalla
Windows añade otra capa de traducción: semánticas NTFS, APIs de notificación de archivos de Windows y la VM Linux de Docker. Si además usas WSL2, puedes acabar con una matrioska de sistemas de ficheros.
Guía práctica:
- Mejor caso en WSL2: mantiene el repositorio dentro del sistema de ficheros Linux (no en una unidad Windows montada), ejecuta Docker/Compose desde allí.
- Peor caso: editar archivos en Windows, montar en un contenedor en una VM Linux y esperar semánticas perfectas de inotify. Ese camino termina en polling.
Soluciones que funcionan: de “suficientemente bueno” a “UX de desarrollo de calidad productiva”
Esta sección es opinada porque intentas enviar código, no organizar un simposio sobre teoría de sistemas de ficheros.
Arreglo nivel 1: Haz que inotify tenga éxito en Linux (límites + alcance)
Si estás en host Linux y aún sufres, haz esto en orden:
- Eleva los límites de inotify (Tarea 5/6).
- Deja de vigilar basura: ignora
node_modules, outputs de build, caches. - Vigila directorios, no archivos individuales, para sobrevivir a guardados atómicos.
- No montes dependencias desde el host: usa un volumen para
node_modules,vendor, etc.
Si haces esas cuatro cosas, la mayoría de los contenedores para desarrollo en hosts Linux se comportan como entornos de desarrollo normales.
Arreglo nivel 2: Polling controlado (cuando el reenvío de eventos es poco fiable)
El polling no da vergüenza. El polling es determinista. El polling también es la razón por la que la batería de tu portátil presenta una queja a RR.HH.
Usa polling cuando:
- los eventos inotify no aparecen para ediciones desde el host (la Tarea 4 muestra el desajuste)
- la plataforma es macOS/Windows y necesitas un arreglo rápido y fiable hoy
Haz el polling tolerable:
- Haz polling solo en los directorios de origen que necesitas.
- Usa intervalos razonables (200–1000ms según el tamaño del repo).
- Desactiva la vigilancia de árboles generados grandes.
Ejemplos que verás en la práctica:
- Herramientas basadas en Chokidar:
CHOKIDAR_USEPOLLING=true, a veces conCHOKIDAR_INTERVAL. - Webpack: watchOptions.poll
- Python watchdog:
WATCHDOG_USE_POLLING=true(depende de la herramienta) - Rails: cambia al file watcher por polling o ajusta el backend de la gema listen
Arreglo nivel 3: Sincroniza el código en un sistema de ficheros nativo del contenedor (el enfoque “aburridamente correcto”)
Si quieres recarga en caliente que se comporte como Linux, dale a tu contenedor un sistema de ficheros Linux para vigilar. Eso significa:
- Pon el árbol de trabajo en un volumen nombrado (rápido, nativo en la VM).
- Sincroniza el código fuente desde el host → volumen (unidireccional o bidireccional) usando una herramienta de sync.
- Ejecuta los watchers dentro del contenedor apuntando a la ruta del volumen.
Esto reduce o elimina los problemas de reenvío de eventos porque la vigilancia ocurre en un sistema de ficheros Linux real, no en un montaje remoto haciéndose pasar por uno.
Hay múltiples maneras de implementar la sincronización:
- Sincronización basada en herramienta (común en equipos serios)
- Funciones “develop” / watch de Compose dependiendo de la versión de Docker y soporte en tu entorno
- Bucle rsync manual si necesitas algo simple y controlado
Arreglo nivel 4: Divide responsabilidades: compila en el host, ejecuta en el contenedor
Esto es herejía en algunas organizaciones y cordura en otras. Si la razón principal para contenedores en dev es “igualar runtime de prod”, todavía puedes compilar activos en el host y montar solo los outputs en el contenedor, o proxyar peticiones.
Cuando funciona bien:
- Herramientas frontend corren en el host (eventos locales rápidos), el contenedor sirve API/backend.
- O el backend corre en el host y dependencias como bases de datos corren en contenedores.
Cuando se convierte en un desastre:
- Cuando tu equipo necesita un solo comando para arrancar todo y tus políticas de red son hostiles al cross-talk en localhost.
Arreglo nivel 5: Reduce la superficie vigilada (tu repo es demasiado grande)
Los monorepos y repos multi-lenguaje no solo “vigilán”. Requieren estrategia:
- Vigila solo el paquete en el que estás trabajando activamente.
- Usa referencias de proyecto y builds incrementales.
- Excluye .git, caches, dependencias vendorizadas y artefactos generados de forma agresiva.
- Considera servicios de watcher dedicados (watchman) si tu stack lo soporta.
La cita única
Idea parafraseada (atribuida): Gene Kim ha enfatizado que la fiabilidad viene de hacer el trabajo visible y reducir la sorpresa, no de heroísmos en el momento.
Tres mini-historias corporativas desde el frente
Mini-historia #1: El incidente causado por una suposición equivocada
Un equipo de producto mediano desplegó “dev containers para todos” tras un trimestre difícil de onboarding. Tenían un stack Compose limpio: API, worker, base de datos y un servidor de desarrollo frontend. Funcionaba genial en laptops Linux. En macOS, algunos desarrolladores empezaron a reportar que al guardar un archivo no se recompilaba, pero solo “a veces”. El lead asumió que era el framework. Naturalmente.
Persiguieron explicaciones a nivel de aplicación durante una semana. Conmutaron ajustes de HMR, cambiaron bundlers, fijaron dependencias e incluso reescribieron parte del script de dev. El problema persistió y la confianza bajó. Los nuevos contratados empezaron a ejecutar servicios directamente en sus máquinas, derrotando la iniciativa.
La causa real fue más simple y embarazosa: la suposición equivocada de que los bind mounts en Docker Desktop se comportan como bind mounts en Linux. Sus watchers dependían de que los eventos inotify llegaran con prontitud y consistencia. En macOS, la capa de traducción de eventos ocasionalmente coalescía eventos durante ráfagas de escrituras (especialmente cuando el editor guardaba varios archivos en rápida sucesión).
La solución no fue mágica. Validaron la falla con inotifywait dentro del contenedor y vieron secuencias faltantes. Luego movieron el árbol de trabajo a un volumen nativo del contenedor y sincronizaron el código. De repente la misma herramienta de vigilancia se comportó perfectamente, porque vigilaba un sistema de ficheros Linux real. El equipo aprendió la lección: “los montajes cross-OS son una capa de compatibilidad, no una garantía”.
Mini-historia #2: La optimización que salió mal
Un equipo empresarial decidió acelerar el desarrollo montando todo desde el host: raíz del repo, caches de dependencias, outputs de build, incluso caches de paquetes de lenguajes. Querían que las recompilaciones en contenedores fueran instantáneas y querían conservar instalaciones de dependencias entre reinicios. En papel, era una ganancia de productividad.
En la práctica, creó un bucle de retroalimentación. Los watchers observaban cambios en outputs y caches, disparaban recompilaciones, que cambiaban outputs otra vez, lo que disparaba más recompilaciones. El uso de CPU no solo subió—se estabilizó en “siempre alto”, que es una forma educada de decir que los portátiles se convirtieron en calefactores.
Peor aún, el sistema de vigilancia empezó a perder cambios reales de código porque se ahogaba en eventos irrelevantes. Los desarrolladores reportaron “la recarga en caliente es poco fiable”, pero el problema subyacente era la sobrecarga de eventos y un alcance de vigilancia patológico. El equipo “lo arregló” activando polling con un intervalo corto. Eso hizo que el problema del ventilador fuera aún mejor, de la misma manera que verter gasolina mejora una fogata.
La solución aburrida: deja de montar caches y artefactos de build desde el host, y deja de vigilarlos. Movieron node_modules y directorios de build a volúmenes nombrados, actualizaron patrones de ignore e hicieron explícito el alcance de vigilancia. El sistema se silenció. La recarga en caliente se volvió fiable. La “optimización” se revirtió porque optimizaba lo equivocado: optimizó la velocidad de rebuild del contenedor a costa de la estabilidad en tiempo de ejecución.
Mini-historia #3: La práctica aburrida pero correcta que salvó el día
Una compañía del sector financiero (cultura de cumplimiento estricta, comportamiento cowboy mínimo) tenía una política de entorno de dev: cada repo debía incluir un script de diagnóstico que imprimiera supuestos del entorno. No era glamoroso. Molestaba a algunos ingenieros. También los salvó repetidamente.
Cuando la vigilancia de archivos empezó a fallar tras una actualización de Docker Desktop, el equipo no discutió sobre frameworks. Ejecutaron el script. Verificó: tipo de montaje, tipo de sistema de ficheros, límites de inotify y si los eventos inotify aparecen al editar desde el host vs dentro del contenedor. Era básicamente las Tareas 1–6 empaquetadas en un comando.
En minutos, tenían un hallazgo claro: las ediciones desde el host no producían eventos inotify dentro del contenedor, mientras que las ediciones dentro del contenedor sí. Eso lo acotó a la capa de compartición de ficheros, no al código de la aplicación. Activaron una flag en su setup de dev: los usuarios macOS pasaban automáticamente a un flujo de trabajo de sync-into-volume, los usuarios Linux se quedaban con bind mounts.
El resultado fue aburrido en el mejor sentido: menos tickets de soporte, menos argumentos de “funciona en mi máquina” y una decisión documentada. La práctica no era ingeniosa; era disciplinada. En términos de ops, redujeron el tiempo medio para encontrar la inocencia de la aplicación.
Errores comunes: síntomas → causa raíz → solución
1) Síntoma: “La recarga en caliente funciona solo tras reiniciar el contenedor”
Causa raíz: el watcher se bloqueó o dejó de funcionar silenciosamente tras alcanzar límites de watch (ENOSPC) o encontrar patrones de rename que no maneja.
Solución: revisa logs por ENOSPC/EMFILE (Tarea 7), eleva límites de inotify (Tarea 6) y configura el watcher para vigilar directorios + manejar guardados atómicos (Tarea 13).
2) Síntoma: “No hay recompilaciones en macOS/Windows, pero sí en host Linux”
Causa raíz: el reenvío de eventos de bind mount a través de la VM de Docker Desktop es poco fiable para tus patrones de cambio.
Solución: confirma con inotifywait (Tarea 3/4). Luego habilita polling con intervalos sensatos (Tarea 12) o pasa a sync-into-volume.
3) Síntoma: “Las recompilaciones ocurren en grandes ráfagas”
Causa raíz: coalescencia de eventos o reenvío retrasado por la capa de compartición; a veces exacerbado por editores que escriben múltiples archivos o formateo al guardar.
Solución: reduce el árbol vigilado; evita vigilar outputs de compilación; considera sincronizar al filesystem del contenedor. El polling puede ayudar si las ráfagas son aceptables.
4) Síntoma: “La CPU se dispara tras activar polling”
Causa raíz: intervalo de polling demasiado corto; árbol vigilado demasiado grande; escaneo de rutas montadas lentas.
Solución: aumenta el intervalo, estrecha el alcance vigilado y elimina rutas pesadas de los bind mounts (Tarea 10). Prefiere sincronizar a un volumen si necesitas recompilaciones de baja latencia.
5) Síntoma: “Algunos directorios desencadenan recarga, otros nunca”
Causa raíz: vigilancia no recursiva (inotify), bugs en la herramienta al añadir watches, o agotamiento de watches durante la inicialización.
Solución: eleva límites de watch y confirma que el watcher reporta el conjunto completo de vigilancias; ignora directorios pesados e incluye explícitamente lo que importa.
6) Síntoma: “Los cambios en el código montado aparecen, pero la herramienta aun así no recompila”
Causa raíz: la herramienta está vigilando una ruta distinta a la montada (común cuando workdir difiere), o está configurada para usar un backend de vigilancia distinto.
Solución: imprime las rutas vigiladas en la configuración de la herramienta; confirma workdir (Tarea 1); ejecuta un inotifywait mínimo contra el mismo directorio.
7) Síntoma: “Bucle de recompilación: guardar desencadena recompilación que desencadena guardado que desencadena recompilación…”
Causa raíz: el watcher incluye el directorio de salida; el formateador o generador escribe dentro del árbol vigilado; el proceso de build toca el árbol fuente.
Solución: excluye outputs, mueve outputs a un directorio separado que no se vigile y mantén los artefactos de build fuera del bind mount si es posible.
8) Síntoma: “El montaje en volumen de Docker lo arregla, pero ahora no puedo editar el código”
Causa raíz: moviste el árbol de trabajo a un volumen pero no añadiste un mecanismo de sincronización.
Solución: adopta una herramienta de sync o un bucle rsync scriptado; sigue editando en el host y sincroniza al volumen, vigilando dentro del contenedor.
Broma #2: El polling es el “¿lo has probado apagar y encender?” de la vigilancia de archivos, excepto que nunca se apaga—solo tu batería.
Listas de verificación / plan paso a paso
Paso a paso: obtén recarga en caliente fiable en menos de una hora
- Demuestra que el montaje es correcto (Tarea 1 y Tarea 2). Si el contenedor no ve los ficheros que editas, detente.
- Demuestra que existen eventos con
inotifywait(Tarea 3). Edita desde el host y observa. - Diferencia reenvío de eventos vs comportamiento de la herramienta (Tarea 4). Si las ediciones dentro del contenedor desencadenan eventos pero las del host no, no es tu framework.
- Comprueba agotamiento de watches (Tarea 5 y Tarea 7). Arregla límites y alcance.
- Elimina directorios pesados de los bind mounts (Tarea 10). Pon dependencias en un volumen nombrado.
- Ignora rutas basura explícitamente en la configuración del watcher (Tarea 11). No confíes en valores por defecto.
- Prueba polling controlado (Tarea 12). Si funciona, tienes una vía a fiabilidad hoy.
- Si estás en macOS/Windows y sigue siendo inestable: adopta sync-into-volume para el árbol de trabajo.
- Documenta la decisión para tu equipo: “Linux usa inotify; macOS usa sync o polling; aquí está el porqué.”
Checklist: “Haz los bind mounts menos dolorosos”
- Montar solo código fuente; mantener dependencias y outputs en volúmenes nombrados.
- Usar patrones de ignore explícitos para las herramientas watcher.
- Mantener árboles vigilados pequeños y predecibles.
- Evitar bucles de vigilancia separando inputs y outputs.
- Preferir vigilancia de directorios sobre archivos individuales.
Checklist: “Cuándo dejar de pelear y cambiar a sync”
inotifywaitmuestra eventos faltantes para ediciones desde el host.- El polling funciona pero consume CPU o es demasiado lento con intervalos tolerables.
- El tamaño del repo exige grandes conjuntos de watch y frecuentemente alcanzas límites.
- Tu equipo usa varios sistemas operativos y necesitas comportamiento consistente entre laptops.
Preguntas frecuentes
1) ¿Por qué la vigilancia de archivos funciona en la máquina Linux de mi compañero pero no en macOS?
En Linux, los contenedores comparten el kernel del host y inotify se comporta normalmente en bind mounts. En macOS, los cambios de fichero deben cruzar una capa de compartición en una VM, y el reenvío de eventos puede estar retrasado o ser imperfecto.
2) ¿Es siempre seguro aumentar fs.inotify.max_user_watches?
Generalmente es seguro en máquinas de desarrollo, pero aumenta el uso de memoria del kernel para llevar el registro de watches. Súbelo porque lo necesitas, no porque lo diga un blog cualquiera. Luego reduce el alcance vigilado de todos modos.
3) ¿Por qué veo ENOSPC si aún tengo espacio en disco?
Porque ENOSPC está sobrecargado: en este contexto significa “no queda espacio para descriptores de watch”, no bloques de disco. Revisa logs (Tarea 7) y sysctls de inotify (Tarea 5/6).
4) ¿Cuál es la solución rápida si necesito fiabilidad hoy?
Habilita polling para tu watcher (Tarea 12), aumenta el intervalo hasta que la CPU sea razonable y reduce el alcance vigilado. Luego planifica una solución a largo plazo (sync-into-volume) si estás en macOS/Windows.
5) ¿Por qué vigilar node_modules causa tantos problemas?
Porque es enorme, cambia con frecuencia y contiene árboles de directorio profundos. Vigilarlo consume watches de inotify y genera ruido. La mayoría de herramientas no necesitan vigilarlo; lo necesitan resuelto.
6) ¿Pueden las funciones de “watch” de Docker Compose reemplazar el watcher de mi herramienta?
A veces. Pueden sincronizar cambios y disparar acciones de rebuild/restart, lo que puede evitar inotify por completo. Pero es otra pieza en movimiento; valida que encaje con tu flujo y no oculte latencia.
7) ¿Por qué los guardados atómicos rompen algunos watchers?
El guardado atómico suele ser “escribir un archivo temporal y luego renombrar”. Si un watcher rastrea un inode de archivo demasiado literalmente, puede perder que el “nuevo” archivo reemplazó al antiguo. Las vigilancias de directorio manejan esto mejor.
8) ¿Debería ejecutar mi servidor de desarrollo en el host y solo contenerizar dependencias?
Si tu principal dolor es la vigilancia de archivos y tu app no requiere paridad de kernel, sí, es una opción pragmática. Solo sé explícito sobre qué “paridad con producción” estás sacrificando.
9) ¿Por qué mover el repo a un volumen nombrado ayuda?
Porque el contenedor vigila un sistema de ficheros Linux nativo dentro de la VM/host, evitando las semánticas de compartición cross-OS. Cambias editabilidad directa por corrección, y luego recuperas editabilidad vía sincronización.
Conclusión: próximos pasos que puedes enviar hoy
La recarga en caliente en contenedores no está “rota” de una sola manera universal. Está rota en formas específicas y repetibles—reenvío de eventos, límites de watch, rendimiento y suposiciones de herramientas. Trátalo como un problema SRE: mide primero, cambia después, documenta tercero.
- Ejecuta
inotifywaitdentro del contenedor y demuestra si los eventos llegan desde ediciones del host. - Si alcanzas límites, aumenta watches de inotify y estrecha el alcance vigilado.
- Si estás en macOS/Windows y los eventos son poco fiables, elige: polling controlado ahora, sync-into-volume para cordura a largo plazo.
- Divide montajes: bind mount para código fuente, volumen para dependencias y outputs.
- Escribe un pequeño script de equipo que ejecute las comprobaciones clave, para que esto no se convierta en conocimiento tribal.
Haz que la vigilancia de archivos sea aburrida. Tu yo futuro te lo agradecerá.