Trampa de propagación de montajes en Docker: por qué tu bind mount parece vacío y cómo solucionarlo

¿Te fue útil?

Montas un directorio del host en un contenedor con bind-mount. Dentro del contenedor está vacío. Verificas la ruta. Pruebas otra shell. Reinicias Docker. Empiezas a negociar con el universo. El directorio del host está lleno de archivos, pero el contenedor insiste en que es un vacío prístino.

Esto a menudo no es un problema de permisos, ni SELinux, ni “Docker siendo Docker”. Es la propagación de montajes: cómo los eventos de montaje (nuevos montajes) se propagan—o no—entre el host y el namespace de montaje del contenedor. Es sutil. Es Linux. Tiene solución.

El síntoma exacto y por qué engaña a gente lista

Definamos el modo de falla “bind mount parece vacío” con precisión, porque “vacío” puede significar tres cosas diferentes:

  1. Realmente vacío: dentro del contenedor ves un directorio vacío, mientras que en el host hay archivos.
  2. Contenido de submount faltante: el directorio tiene archivos, pero un sistema de ficheros montado debajo de él (como /mnt/data o /var/lib/kubelet/pods) aparece vacío o como un directorio normal.
  3. Vista obsoleta: el contenedor ve contenido antiguo, pero los montajes realizados después del arranque del contenedor no aparecen.

El primer caso suele ser permisos, desajuste UID/GID, o SELinux/AppArmor. Pero el segundo y el tercero son errores clásicos de propagación de montajes. Montaste algo debajo del bind mount en el host y el contenedor no recibió la notificación.

La trampa mental: los bind mounts se sienten como “una ventana al host”. No lo son. Son una referencia a una ruta, resuelta en un namespace de montaje, con semántica de propagación específica. Linux está haciendo exactamente lo que pediste, no lo que querías.

Broma corta #1: Si alguna vez quieres sentirte poderoso, crea un namespace de montaje; puedes hacer que / signifique lo que quieras. Es como viajar en el tiempo, pero con más flags de mount.

Hechos e historia interesantes (corto, concreto, útil)

  • La propagación de montajes llegó al kernel Linux en la era 2.6 como parte del esfuerzo mayor por soportar contenedores: namespaces de montaje separados más compartición controlada entre ellos.
  • La característica de “subárbol compartido” existe por los montajes recursivos. Un simple bind mount no es suficiente cuando quieres que los eventos de montaje debajo de un directorio aparezcan en otros lugares.
  • Docker originalmente se apoyó mucho en los valores por defecto de Linux. Esos valores cambian según la distribución y la configuración del sistema init, por eso el mismo comando Docker puede comportarse distinto en distintos hosts.
  • Systemd cambió las reglas del juego. Muchos sistemas gestionados por systemd dejan / (o puntos de montaje clave) como shared por defecto, lo que afecta cómo los montajes se propagan hacia servicios y contenedores.
  • Kubernetes expone explícitamente la propagación de montajes (mountPropagation: HostToContainer, etc.) porque los plugins de almacenamiento y los drivers CSI se encuentran con estos problemas constantemente.
  • “rshared” no es “más permisos de acceso”. Se trata de eventos de montaje, no de acceso a ficheros. Puedes tener permisos perfectos y aun así ver submounts vacíos.
  • OverlayFS hizo baratos los rootfs de contenedores pero no eliminó la necesidad de entender montajes; facilitó que se olvide que estás apilando sistemas de ficheros.
  • Los bind mounts pueden ocultar montajes. Si haces un bind mount sobre un punto de montaje existente, ocultas lo que estaba montado allí. A veces “vacío” es “montaste encima”.
  • Los runtimes de contenedores eligen valores por defecto conservadores. Muchos usan rprivate para los bind mounts para evitar que los contenedores influyan en la tabla de montajes del host.

Propagación de montajes: qué está pasando realmente

Linux tiene un concepto llamado namespace de montaje. Cada namespace tiene su propia vista de la tabla de montajes: qué está montado dónde y con qué flags. Los contenedores usan esto para que un contenedor pueda montar cosas sin alterar la tabla de montajes del host (y viceversa).

Pero los montajes no son solo una tabla estática. Son eventos. Un proceso puede montar un sistema de ficheros en /mnt/thing después de que tu contenedor haya arrancado. Si ese nuevo montaje aparece dentro del contenedor depende de la propagación de montajes entre los dos namespaces.

Shared, private, slave: las tres personalidades

Cada punto de montaje tiene un tipo de propagación. En la práctica verás:

  • private: los eventos de montaje/desmontaje bajo este montaje no se propagan a pares.
  • shared: los eventos de montaje/desmontaje se propagan a todos los montajes del mismo grupo de pares.
  • slave: recibe propagación de un montaje shared maestro, pero no envía eventos de vuelta.

Luego están las versiones recursivas:

  • rshared, rprivate, rslave: aplican la regla al montaje y a todo lo que hay debajo.

Aquí está la trampa: los bind mounts de Docker a menudo se crean con un modo de propagación que no es recursivamente shared. Así que si el host monta algo debajo de la ruta origen más tarde, el contenedor no lo verá.

Un patrón real común que desencadena el error

Haces algo como:

  • Bind-mount /srv en el contenedor como /srv.
  • En el host, más tarde montas un NFS en /srv/data (o systemd lo monta bajo demanda).
  • Dentro del contenedor, /srv/data parece vacío o como un directorio normal, porque el submount no se propagó.

Eso no es un problema de visibilidad de archivos. Es un problema de visibilidad de submount.

Una cita, porque la gente de ops colecciona estas como cicatrices

Everything fails all the time. — Werner Vogels

Guía rápida de diagnóstico

Si un bind mount parece vacío, puedes perder una tarde—o puedes ejecutar esta lista de verificación en cinco minutos y saber dónde está el cadáver.

  1. Confirma qué tipo de “vacío” es. ¿Faltan archivos, o solo el contenido de submounts?
  2. Comprueba si la ruta del host contiene un punto de montaje debajo. Si es así, sospecha propagación inmediatamente.
  3. Inspecciona la configuración de propagación del mount en el contenedor. Busca Propagation en la salida de docker inspect.
  4. Compara vistas de namespaces de montaje. Usa nsenter para ver los montajes tal como los ve el contenedor.
  5. Decide: cambia la propagación, cambia la arquitectura, o deja de montar bajo esa ruta. Hay una solución “correcta” y una solución para “detener la hemorragia”. Elige conscientemente.

Atajo: si montas cosas dinámicamente bajo un directorio y además bind-mounts ese directorio en contenedores, casi siempre querrás rshared o rslave. El rprivate por defecto es seguro pero sorprendente.

Tareas prácticas: comandos, salidas, decisiones (12+)

Task 1: Reproducir el error en un entorno controlado

cr0x@server:~$ sudo mkdir -p /srv/demo/submount
cr0x@server:~$ echo "host-file" | sudo tee /srv/demo/host.txt
host-file
cr0x@server:~$ docker run --rm -d --name mp-test -v /srv/demo:/demo alpine sleep 100000
b8a3b2c55d9f8e8a2c5a2b0f0d2f0b6d9a5f9d0c8b7a0d1c2b3a4f5e6d7c8b9
cr0x@server:~$ docker exec mp-test ls -la /demo
total 8
drwxr-xr-x    3 root     root          4096 Jan  3 12:00 .
drwxr-xr-x    1 root     root          4096 Jan  3 12:00 ..
-rw-r--r--    1 root     root             10 Jan  3 12:00 host.txt
drwxr-xr-x    2 root     root          4096 Jan  3 12:00 submount

Qué significa: el bind mount básico funciona. El contenedor ve los ficheros existentes.

Decisión: procede a montar algo bajo /srv/demo/submount en el host y comprueba si el contenedor lo hereda.

Task 2: Crear un montaje bajo el directorio bind-montado (lado host)

cr0x@server:~$ sudo mount -t tmpfs tmpfs /srv/demo/submount
cr0x@server:~$ echo "mounted-file" | sudo tee /srv/demo/submount/mounted.txt
mounted-file
cr0x@server:~$ mount | grep "/srv/demo/submount"
tmpfs on /srv/demo/submount type tmpfs (rw,relatime)

Qué significa: ahora hay un submount bajo el directorio que bindaste en el contenedor.

Decisión: comprueba si el contenedor ve mounted.txt. Si no, es propagación.

Task 3: Comprobar visibilidad dentro del contenedor

cr0x@server:~$ docker exec mp-test sh -lc "mount | grep /demo/submount || true; ls -la /demo/submount; cat /demo/submount/mounted.txt 2>/dev/null || echo 'no mounted.txt'"
total 8
drwxr-xr-x    2 root     root          4096 Jan  3 12:00 .
drwxr-xr-x    3 root     root          4096 Jan  3 12:00 ..
no mounted.txt

Qué significa: el contenedor no ve el submount tmpfs; en su lugar ve el directorio antiguo.

Decisión: confirma el modo de propagación y corrige con rshared/rslave según tus necesidades de seguridad.

Task 4: Inspeccionar la configuración de propagación del bind mount

cr0x@server:~$ docker inspect mp-test --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/demo","Destination":"/demo","Mode":"","RW":true,"Propagation":"rprivate"}]

Qué significa: la propagación es rprivate. Los montajes nuevos bajo /srv/demo no aparecerán en el contenedor.

Decisión: cambia a rshared o rslave en este montaje.

Task 5: Reejecutar el contenedor con propagación rshared

cr0x@server:~$ docker rm -f mp-test
mp-test
cr0x@server:~$ docker run --rm -d --name mp-test -v /srv/demo:/demo:rshared alpine sleep 100000
a0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
cr0x@server:~$ docker inspect mp-test --format '{{range .Mounts}}{{.Destination}} {{.Propagation}}{{"\n"}}{{end}}'
/demo rshared

Qué significa: el bind mount ahora está configurado para propagar submounts.

Decisión: valida que el submount tmpfs existente se vuelva visible.

Task 6: Validar visibilidad del submount después de cambiar la propagación

cr0x@server:~$ docker exec mp-test sh -lc "mount | grep /demo/submount; ls -la /demo/submount; cat /demo/submount/mounted.txt"
tmpfs on /demo/submount type tmpfs (rw,relatime)
total 4
drwxr-xr-x    2 root     root            60 Jan  3 12:01 .
drwxr-xr-x    3 root     root          4096 Jan  3 12:01 ..
-rw-r--r--    1 root     root            13 Jan  3 12:01 mounted.txt
mounted-file

Qué significa: el contenedor ahora ve el submount del host. La corrección de propagación está confirmada.

Decisión: decide si rshared es aceptable o si prefieres rslave para propagación unidireccional.

Task 7: Elegir rslave cuando quieras propagación unidireccional

cr0x@server:~$ docker rm -f mp-test
mp-test
cr0x@server:~$ docker run --rm -d --name mp-test -v /srv/demo:/demo:rslave alpine sleep 100000
d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9
cr0x@server:~$ docker exec mp-test sh -lc "mount | grep /demo/submount; cat /demo/submount/mounted.txt"
tmpfs on /demo/submount type tmpfs (rw,relatime)
mounted-file

Qué significa: rslave aún permite eventos host → contenedor, pero no propaga los montajes del contenedor de vuelta al host.

Decisión: en la mayoría de casos de producción, prefiere rslave a menos que necesites explícitamente propagación bidireccional.

Task 8: Confirmar el estado de propagación del montaje en el host (shared vs private)

cr0x@server:~$ findmnt -o TARGET,PROPAGATION /srv/demo
TARGET   PROPAGATION
/srv/demo shared

Qué significa: el punto de montaje del host puede estar en un grupo de pares compartido. Si está private, puede que necesites cambiar también el lado del host.

Decisión: si la propagación es private en el host y necesitas compartir, puede que debas usar mount --make-rshared (con precaución).

Task 9: Ver de forma segura las diferencias de namespace de montaje usando nsenter

cr0x@server:~$ pid=$(docker inspect -f '{{.State.Pid}}' mp-test)
cr0x@server:~$ sudo nsenter -t "$pid" -m -- findmnt -R /demo | head
TARGET            SOURCE         FSTYPE OPTIONS
/demo             /dev/sda1      ext4   rw,relatime
/demo/submount    tmpfs          tmpfs  rw,relatime

Qué significa: estás viendo los montajes desde dentro del namespace de montaje del contenedor sin depender de herramientas del contenedor.

Decisión: si la vista del namespace no incluye el submount, es propagación o sincronización. Si lo incluye, la ruta de tu aplicación está equivocada.

Task 10: Detectar errores de “montado encima” que ocultan datos

cr0x@server:~$ sudo mount -t tmpfs tmpfs /srv/demo
cr0x@server:~$ ls -la /srv/demo | head
total 8
drwxr-xr-x  2 root root   40 Jan  3 12:02 .
drwxr-xr-x  3 root root   18 Jan  3 12:00 ..

Qué significa: acabas de montar tmpfs sobre /srv/demo, ocultando el contenido original. Felicidades, creaste “vacío” de la manera honesta.

Decisión: desmonta y replantea: no montes sobre tu origen bind a menos que quieras reemplazarlo.

Task 11: Inspeccionar montajes con conciencia de recursión

cr0x@server:~$ sudo umount /srv/demo
cr0x@server:~$ findmnt -R /srv/demo
TARGET            SOURCE  FSTYPE OPTIONS
/srv/demo         /dev/sda1 ext4  rw,relatime
/srv/demo/submount tmpfs   tmpfs  rw,relatime

Qué significa: -R muestra submounts. Si solo ejecutas mount | grep /srv/demo podrías perder el montaje anidado.

Decisión: si los “datos faltantes” están en un submount, trátalo como propagación de montajes hasta que se demuestre lo contrario.

Task 12: Verificar problemas de etiquetas SELinux (para evitar falsos positivos)

cr0x@server:~$ docker run --rm -v /srv/demo:/demo:Z alpine sh -lc "ls -la /demo | head"
total 8
drwxr-xr-x    3 root     root          4096 Jan  3 12:00 .
drwxr-xr-x    1 root     root          4096 Jan  3 12:03 ..
-rw-r--r--    1 root     root             10 Jan  3 12:00 host.txt

Qué significa: si estás en sistemas con SELinux en modo enforcing, la opción :Z relabela contenido para acceso del contenedor. Si esto arregla el “vacío”, no era propagación.

Decisión: separa el diagnóstico: “denegación de política o permisos” vs “submount que no se propaga”. Distintas soluciones, distintos riesgos.

Task 13: Confirmar que el perfil AppArmor no esté bloqueando operaciones de montaje (raro pero desagradable)

cr0x@server:~$ docker inspect mp-test --format '{{.AppArmorProfile}}'
docker-default

Qué significa: el contenedor se ejecuta bajo un perfil (común en Ubuntu). Normalmente esto afecta intentos de montaje desde dentro del contenedor, no la visibilidad de montajes del host.

Decisión: si tu contenedor debe realizar montajes (p. ej. FUSE, comportamiento tipo CSI), puede que necesites privilegios extra o un diseño distinto.

Task 14: Comprobar si la ruta origen es un punto de montaje con propagación “private”

cr0x@server:~$ findmnt -o TARGET,PROPAGATION /
TARGET PROPAGATION
/      shared

Qué significa: si / o el mount padre relevante es private, puedes obtener comportamientos sorprendentes con montajes anidados. Los valores por defecto de la distro importan.

Decisión: no ejecutes a ciegas mount --make-rshared / en producción. Hazlo solo si entiendes el radio de impacto y, idealmente, en ventana de mantenimiento.

Task 15: Ver exactamente qué piensa Docker que montó

cr0x@server:~$ docker inspect mp-test --format '{{range .Mounts}}{{println .Source "->" .Destination "prop:" .Propagation}}{{end}}'
/srv/demo -> /demo prop: rslave

Qué significa: la vista de Docker suele ser suficiente para detectar el problema: rprivate cuando esperabas comportamiento compartido.

Decisión: si Docker dice rprivate, no discutas. Cambia la opción de montaje o cambia la estructura.

Soluciones que funcionan (y cuándo usarlas)

Solución 1: Establecer la propagación explícitamente en el bind mount

Para la CLI de Docker, puedes indicarlo en la especificación del volumen:

  • Bidireccional: -v /path:/path:rshared
  • Solo host→contenedor (usualmente más seguro): -v /path:/path:rslave

Cuándo usar: cuando el host monta o desmonta cosas bajo la ruta origen después del arranque del contenedor (automounts NFS, montajes systemd, staging CSI, montajes loop, tmpfs temporales).

Cuándo no usar: si no controlas lo que el contenedor puede hacer y te preocupa que eventos de montaje fluyan de vuelta (evita rshared salvo que lo necesites).

Solución 2: Dejar de montar bajo un directorio bind-montado

Esta es la solución arquitectónica aburrida: no crees submounts bajo una ruta que estás bind-montando en contenedores. Monta tu sistema de ficheros dinámico en otro lugar y luego bind-móntalo ya montado directamente en el contenedor.

Ejemplo: en lugar de bind-montar /srv y después montar NFS en /srv/data, monta NFS en /mnt/nfs/data y bind-monta /mnt/nfs/data directamente.

Por qué funciona: evitas depender de la propagación por completo. Menos magia. Menos sorpresas.

Solución 3: Hacer el montaje del host shared (solo si es imprescindible)

Si el punto de montaje en el host es private, incluso un bind mount de contenedor marcado rshared puede no dar el resultado esperado. En algunas configuraciones, necesitas que el árbol de montajes del host sea shared.

cr0x@server:~$ sudo mount --make-rshared /srv
cr0x@server:~$ findmnt -o TARGET,PROPAGATION /srv
TARGET PROPAGATION
/srv   shared

Qué significa: los eventos de montaje bajo /srv pueden propagarse a montajes pares.

Decisión: haz esto solo para un subárbol que controles. Hacer / recursivamente shared puede interactuar de forma sorprendente con otros servicios.

Solución 4: Preferir la sintaxis --mount por claridad

La sintaxis -v es compacta pero fácil de leer mal. La forma --mount es verborreica y menos ambigua:

cr0x@server:~$ docker run --rm -d --name mp-test \
  --mount type=bind,source=/srv/demo,target=/demo,bind-propagation=rslave \
  alpine sleep 100000
2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1

Por qué te debe importar: los incidentes de producción aman las configuraciones que “parecían correctas”. La configuración verbosa parece correcta menos a menudo, lo cual es una victoria.

Solución 5: Si tu contenedor necesita montar cosas, replantea el diseño

Algunas cargas de trabajo (herramientas de backup, agentes de almacenamiento, FUSE, componentes tipo plugin) quieren montar dentro del contenedor y que eso aparezca en el host o en contenedores hermanos. Ahí es donde aparecen rshared y privilegios, y también donde las revisiones de seguridad suelen morir.

Guía con opinión: si necesitas que montajes iniciados por contenedores aparezcan en el host, estás construyendo un componente de almacenamiento. Trátalo como tal: mínimo privilegio, restricciones estrictas y propagación explícita. Si no lo necesitas, no lo actives “por si acaso”.

Broma corta #2: “Solo ejecútalo privilegiado” es el equivalente ops de “¿lo has apagado y encendido?”, salvo que habilita nuevas formas de romper cosas.

Tres mini-historias del mundo corporativo

Mini-historia 1: El incidente causado por una suposición equivocada

Un equipo migró un sistema batch legado a contenedores. Tenía un contrato simple: dejar ficheros en /incoming, procesarlos y escribir resultados en /outgoing. En las VMs eran solo directorios. En la era contenedores bind-montaron /srv/pipeline en el contenedor como /pipeline y mantuvieron las rutas internas iguales.

Meses después, el uso de almacenamiento se disparó. Alguien añadió una unidad automount para que /srv/pipeline/incoming estuviera respaldada por NFS durante horas de trabajo. Fue una idea limpia: menos disco local, escalado fácil. El despliegue salió un viernes porque el cambio era “solo almacenamiento”.

Ese fin de semana la flota de workers contenedorizados empezó a reportar “sin ficheros de entrada”. Las métricas eran extrañas: el share NFS estaba lleno de ficheros, el controlador batch lo alimentaba correctamente y los workers parecían saludables. El on-call hizo lo que hace el on-call: reinició workers. Nada cambió.

La causa raíz fue la propagación de montajes. Los workers se iniciaron antes de que el automount se disparara, así que dentro de sus namespaces de montaje, /pipeline/incoming quedó como un directorio simple para siempre. El NFS se montó en el host, pero su bind mount era rprivate. Nunca vieron el submount NFS.

La solución fue aburrida e inmediata: cambiar el bind mount a rslave y redeploy. La solución perdurable fue más importante: dejar de bind-montar un padre amplio como /srv/pipeline; montar NFS en una ruta estable y bind-montar ese directorio exacto. Eso redujo el radio de impacto cuando cambiaba el almacenamiento.

Mini-historia 2: La optimización que salió mal

Un equipo de plataforma quería aceleración en el bootstrap de nodos. Movieron varios datasets grandes del disco local a montajes bajo demanda: algunos eran imágenes loop-mounted, otros shares de red. La idea era “activar” montajes solo cuando un nodo ejecutara la carga que los necesitara.

También estandarizaron argumentos de ejecución de contenedores: cada servicio recibió un bind mount de /opt/runtime para que herramientas comunes y certificados estuvieran disponibles. Funcionó en dev. Funcionó en staging. Producción empezó a fallar de una forma que parecía corrupción de datos: aplicaciones veían directorios vacíos donde debían estar datasets.

Resultó que el script de “activación” montaba datasets bajo /opt/runtime/datasets después de que los contenedores ya estuvieran en marcha. El bind mount era rprivate, así que los contenedores nunca obtuvieron los nuevos submounts. Los ingenieros intentaron “arreglarlo” reiniciando solo las apps afectadas, lo que a veces funcionaba—porque el montaje estaba activo antes del reinicio. Lo llamaron flaky. Era determinista, solo mal sincronizado.

Cambiaron el montaje a rshared globalmente porque hizo desaparecer el problema rápidamente. Dos semanas después, otro incidente: un contenedor de depuración con capacidades extra montó un tmpfs en un lugar que se propagó de vuelta al subtree del host. No destruyó datos, pero confundió el monitoring y rompió scripts de limpieza.

El estado final fue matizado: rslave para la mayoría de servicios, conjuntos de capacidades estrictos y una regla de que solo un agente dedicado puede realizar montajes. La “optimización” no estaba mal; las suposiciones sobre propagación y privilegios sí lo estaban.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Otra organización tenía una flota con almacenamiento complicado: caches SSD locales, montajes de red y remounts ocasionales por incidentes. Tenían una regla simple: no bind-montar directorios padre amplios. Solo montar exactamente lo que el contenedor necesita. Si un contenedor necesita /data/app, recibe /data/app, no /data.

No era popular. A los desarrolladores les gustaba la conveniencia. A los SREs también, hasta que los llamaron. El equipo de plataforma reforzó la regla en revisiones de código y en cheques CI para specs de Compose y manifiestos de despliegue.

Un día, un mantenimiento de almacenamiento requirió remonteo de un subtree bajo /data. En hosts donde los servicios habían bind-montado históricamente /data, esto habría causado bugs de visibilidad parcial. Pero aquí, la mayoría de servicios tenían montajes directos de sus rutas exactas, y solo un par de agentes especializados usaban rslave porque esperaban submounts.

El incidente fue aburrido. Unos cuantos contenedores se reiniciaron como parte del mantenimiento, pero no hubo el misterioso comportamiento de “directorio vacío”. El postmortem fue corto y poco emocionante, que es el mayor cumplido que puedes dar a una práctica de operaciones.

Errores comunes: síntomas → causa raíz → solución

1) “El directorio está vacío en el contenedor, pero no en el host”

  • Síntoma: ls muestra vacío; sin errores.
  • Causa raíz: montaste sobre la ruta origen en el host (mascaramiento), o estás viendo un submount que no se propagó.
  • Solución: ejecuta findmnt -R /host/path para ver submounts; evita montar sobre la fuente del bind; establece bind-propagation=rslave si los submounts deben aparecer.

2) “Los ficheros aparecen tras reiniciar el contenedor”

  • Síntoma: reiniciar el contenedor “lo arregla”, pero solo a veces.
  • Causa raíz: dependencia de tiempo: a veces el montaje del host existe al arrancar el contenedor, otras veces se crea después (automount, scripts bajo demanda).
  • Solución: usa rslave/rshared para propagación, o asegúrate de que los montajes estén activos antes de arrancar contenedores (ordenamiento con systemd).

3) “Solo subdirectorios están vacíos”

  • Síntoma: archivos base visibles, pero un directorio anidado está vacío.
  • Causa raíz: ese directorio anidado es en realidad un punto de montaje en el host.
  • Solución: bind-monta el punto de montaje anidado directamente, o habilita propagación para el bind mount padre.

4) “Funciona en Ubuntu pero no en otra distro (o viceversa)”

  • Síntoma: comando de contenedor idéntico, resultados diferentes entre hosts.
  • Causa raíz: distintos valores por defecto de propagación para / o puntos de montaje relevantes; systemd difiere; runtimes de contenedores difieren.
  • Solución: deja de fiarte de valores por defecto; declara la propagación explícitamente; verifica con findmnt -o TARGET,PROPAGATION.

5) “Pusimos rshared y ahora aparecen montajes raros en el host”

  • Síntoma: el host ve montajes creados por contenedores, o montajes que persisten tras salir el contenedor.
  • Causa raíz: rshared es bidireccional; el contenedor tiene capacidad para montar (o es privilegiado) y los eventos de montaje se propagan de vuelta.
  • Solución: usa rslave para propagación unidireccional; reduce capacidades del contenedor; evita contenedores privilegiados; asegúrate de que un agente host gestione la limpieza de montajes.

6) “El montaje está, pero el acceso está denegado”

  • Síntoma: los ficheros existen pero ves errores de permiso; a veces parece “vacío” para aplicaciones.
  • Causa raíz: mismatch de contexto SELinux, restricciones AppArmor, desajuste UID/GID, limitaciones de Docker rootless.
  • Solución: en sistemas SELinux usa :Z o etiquetas correctas; alinea UID/GID; no confundas denegaciones de política con problemas de propagación.

7) “Bind-montamos un symlink y obtuvimos la ruta equivocada”

  • Síntoma: el contenedor ve un directorio distinto al esperado.
  • Causa raíz: el bind mount resuelve symlinks en tiempo de montaje; los cambios posteriores no lo afectan; o el symlink apunta a un montaje que no se propaga.
  • Solución: bind-monta rutas reales; evita indirection por symlinks en puntos de almacenamiento; verifica con readlink -f en el host.

Listas de verificación / plan paso a paso

Plan paso a paso: arreglar un incidente de producción “bind mount vacío” sin adivinar

  1. Identifica lo que exactamente falta. ¿Son ficheros, o el contenido de un submount? Si es un submount, muévete rápido hacia diagnóstico de propagación.
  2. En el host, mapea el árbol de montajes. Usa findmnt -R en la fuente del bind. Si ves un montaje anidado, encontraste un candidato culpable.
  3. Confirma la propagación del mount en el contenedor. docker inspect y mira Propagation. Si es rprivate, deja de esperar submounts.
  4. Decide la dirección de propagación más segura.
    • Necesitas host → contenedor solo: elige rslave.
    • Necesitas ambas direcciones: elige rshared y justifícalo por escrito.
  5. Implementa explícitamente vía --mount. Despliega y reinicia los contenedores que dependen de ello.
  6. Verifica con nsenter o mount dentro del contenedor. No confíes solo en logs de la aplicación como señal.
  7. Luego arregla la arquitectura. Prefiere montajes directos de lo que necesitas en lugar de bind-montar padres amplios. Esto previene el próximo incidente.

Lista: cuándo deberías usar rslave/rshared

  • Usas unidades automount de systemd bajo un directorio bind-montado en contenedores.
  • Montas almacenamiento de red (NFS, CIFS) o dispositivos de bloque bajo un directorio que los contenedores ya usan.
  • Tienes un “sidecar” o agente host que monta datasets tarde (después de arrancar contenedores).
  • Estás haciendo algo tipo CSI, plugins o integraciones de almacenamiento.

Lista: cuándo evitar rshared

  • El contenedor es privilegiado o tiene capacidades relacionadas con montajes y no necesitas que eventos vayan de vuelta al host.
  • Estás depurando y te tienta “hacer que funcione rápido”.
  • No puedes garantizar el comportamiento de limpieza para montajes creados por procesos de contenedor.

Preguntas frecuentes

1) ¿Por qué mi bind mount muestra archivos, pero los sistemas de ficheros montados debajo faltan?

Porque el bind mount base está presente, pero los eventos de submount no se propagaron al namespace del contenedor. Establece bind-propagation=rslave o rshared, o bind-monta el submount directamente.

2) ¿Cuál es la diferencia práctica entre rshared y rslave?

rshared propaga eventos de montaje/desmontaje en ambas direcciones entre montajes pares. rslave permite propagación host → contenedor pero no contenedor → host. Para la mayoría de servicios en producción, rslave es la opción sensata cuando necesitas propagación.

3) ¿La propagación de montajes puede hacer que un directorio parezca completamente vacío?

Sí, si los “datos reales” están en un montaje anidado (NFS, tmpfs, loop device) y el contenedor solo ve el directorio subyacente. Parecerá vacío o desactualizado porque estás mirando la capa equivocada.

4) ¿Esto es un bug de Docker?

No. Es comportamiento de namespaces de montaje de Linux más los valores por defecto prudentes de Docker. El “bug” es asumir que un bind mount es automáticamente recursivo y dinámico para submounts.

5) ¿Esto afecta también a los volúmenes Docker (named volumes)?

Puede, pero es más común con bind mounts porque mapeas una ruta arbitraria del host donde pueden aparecer submounts sorpresa. Los volúmenes nombrados se gestionan bajo el área de almacenamiento de Docker y normalmente no tienen submounts sorpresa—a no ser que los crees.

6) ¿Cómo interactúa systemd automount con contenedores?

Automount significa que el montaje ocurre en el primer acceso, potencialmente después de que el contenedor arrancó. Si la vista del contenedor no recibe ese evento por rprivate, puede que nunca vea el sistema de ficheros montado. Usa rslave y considera ordenar montajes antes de arrancar contenedores cuando sea posible.

7) ¿Y Docker rootless?

Los entornos rootless añaden restricciones: puede que no puedas cambiar cierta propagación de montajes o realizar montajes en absoluto. Si dependes de montajes dinámicos del host que aparezcan dentro de un contenedor rootless, replantea la arquitectura para evitar esa dependencia.

8) ¿Kubernetes lo resuelve automáticamente?

No. Kubernetes lo hace explícito. Los pods pueden solicitar modos de propagación de montajes, y el cluster debe permitirlo. Si ejecutas plugins de almacenamiento o necesitas que montajes del host aparezcan en contenedores, seguirás enfrentándote a las mismas semánticas de Linux subyacentes.

9) ¿Cuál es el patrón a largo plazo más seguro para evitar esta clase de errores?

No bind-montes padres amplios como /srv o /data. Monta el almacenamiento en puntos dedicados y estables y bind-monta solo los directorios exactos que tu contenedor necesita.

Conclusión: próximos pasos aplicables hoy

Si recuerdas una cosa: un bind mount no es un flujo en vivo de eventos de montaje a menos que lo conviertas en uno. Cuando tu contenedor ve un directorio vacío pero el host tiene datos, pregúntate primero: “¿Esos datos están realmente en un submount?”

Pasos prácticos:

  1. En el host afectado, ejecuta findmnt -R en tu origen bind y localiza montajes anidados.
  2. Ejecuta docker inspect y revisa el campo Propagation del montaje.
  3. Si necesitas visibilidad host → contenedor de submounts, redeploy con bind-propagation=rslave (o rshared solo si está justificado).
  4. Luego refactoriza: bind-monta los puntos de montaje exactos que necesita tu app, no todo el directorio padre.

Esa es la diferencia entre una solución puntual y no volver a ver esta página a las 03:00.

← Anterior
Docker “manifest unknown”: etiquetas vs digests explicado (y solucionado)
Siguiente →
La consola de Proxmox no se abre (SPICE/noVNC): dónde falla y cómo arreglarlo

Deja un comentario