Volúmenes Docker: montajes bind vs volúmenes nombrados — qué sobrevive mejor a las migraciones

¿Te fue útil?

No te das cuenta de cuánto de tu aplicación es “solo archivos” hasta que intentas moverla. Un host muere, un equipo de cloud “amablemente”
reemplaza instancias, o decides cambiar de distribución porque un CVE arruinó tus planes del fin de semana. De repente tus contenedores
son portables y tus datos no lo son.

Los montajes bind y los volúmenes nombrados ambos parecen “almacenamiento persistente” en Docker. No son equivalentes. Uno migra como un
sistema de archivos normal. El otro migra como un detalle de implementación de Docker. Si eliges mal, la migración funciona—hasta la
primera vez que la necesitas.

La tesis con opinión: qué sobrevive mejor a las migraciones

Si tu prioridad principal es “puedo tomar el almacenamiento de este host y reubicarlo con el menor drama posible”,
los montajes bind sobreviven mejor a las migracionescuando controlas el diseño del sistema de archivos del host.
Son explícitos, visibles y transferibles con las mismas herramientas en las que ya confías para mover datos: rsync, snapshots,
backups, replicación, restauraciones, sumas de comprobación. Puedes razonar sobre ellos sin pedir permiso a Docker.

Si tu prioridad principal es “quiero que Docker gestione la ubicación y el ciclo de vida del almacenamiento y me da igual tratarlo como
un artefacto de la aplicación”, los volúmenes nombrados están bien—y a menudo son más sencillos para las operaciones del día 2
dentro de un mismo host. Reducen errores humanos como typos en rutas y quedan más limpios en stacks de Compose. Pero no se migran automáticamente bien.
Tienes que exportarlos/importarlos a propósito.

Mi elección por defecto en producción es aburrida:

  • Bases de datos y servicios con estado: montaje bind a un directorio del host que esté en un sustrato de almacenamiento real
    que ya respaldas (LVM, dataset ZFS, volumen EBS, LUN SAN, lo que tu organización llame “almacenamiento”).
  • Datos mutables a nivel de aplicación que se pueden reconstruir: los volúmenes nombrados son aceptables, a veces preferibles.
  • Cualquier cosa que podrías necesitar restaurar bajo presión: evita el almacenamiento “solo Docker” a menos que hayas practicado exportar volúmenes.

Una opinión más: las migraciones fallan porque la gente confunde “portabilidad del contenedor” con “portabilidad de los datos.”
El contenedor es la parte fácil. Los bytes son el trabajo.

Qué sucede realmente en el disco

Montajes bind: Docker como invitado, tu sistema de archivos como fuente de verdad

Un montaje bind es Docker diciendo: “Toma este /path/on/host y preséntalo en /path/in/container.”
Docker no es el propietario de los datos. Docker ni siquiera lo finge. La ruta del host existe, se aplican permisos, políticas SELinux/AppArmor
y se aplican las instantáneas o la replicación del equipo de almacenamiento.

Implicación para la migración: si puedes mover esa ruta del host—moviendo el disco subyacente, snapshotteando un dataset, restaurando un backup,
o rsyncando directorios—puedes mover los datos. Docker no añade un formato especial.

Volúmenes nombrados: Docker como casero (y tú alquilas espacio)

Un volumen nombrado es Docker diciendo: “Voy a asignar almacenamiento para este volumen, gestionado por un driver de volúmenes.”
La mayoría de las veces, el driver es local. En Linux con el motor por defecto, eso suele significar que Docker almacena los datos del volumen bajo:
/var/lib/docker/volumes/<volume>/_data.

Esa ruta no es una API estable. Es un detalle de implementación que cambia con:
el driver de almacenamiento, el modo rootless, la capa VM de Docker Desktop, el empaquetado de la distribución y cualquier política que tu equipo de seguridad imponga el próximo trimestre.

Implicación para la migración: para mover volúmenes nombrados de forma segura, trátalos como un activo gestionado por Docker:
inspeccionar, exportar, mover, importar, verificar. No “simplemente copies /var/lib/docker” a menos que hayas probado exactamente esa versión del motor,
el sistema de archivos y la combinación del driver de almacenamiento. A veces funciona. A veces funciona hasta que deja de funcionar.

Chiste #1: Los volúmenes Docker son como plantas de oficina: nadie recuerda quién las posee hasta que hay que mudar el edificio.

Hechos y contexto histórico (lo que explica el desorden actual)

  1. Docker originalmente fomentó contenedores efímeros (cultura de 2013): “reconstruye, no parchees.”
    Los datos persistentes se externalizaron intencionalmente, por eso los volúmenes se sienten añadidos y no nativos.
  2. Los volúmenes nombrados fueron una solución de usabilidad para “montar todo con bind”, que se volvía desordenado en archivos Compose y frágil entre hosts
    con layouts de directorios distintos.
  3. El driver local no es un contrato universal. En Linux mapea al sistema de archivos del host;
    en Docker Desktop mapea al sistema de archivos de una VM que no controlas de la misma manera.
  4. Los drivers de almacenamiento cambiaron con el tiempo (AUFS → overlay2 se volvió común). El comportamiento de “copiar el directorio de datos de Docker”
    está fuertemente ligado al formato en disco del driver de almacenamiento.
  5. Compose v2 hizo que “volumen nombrado por defecto” fuera más común porque es conveniente y evita suposiciones de rutas.
    La conveniencia es cómo acumulas deuda técnica con una sonrisa.
  6. Rootless Docker y namespaces de usuario convirtieron problemas de permisos de volumen de “caso raro” a “sorpresa rutinaria”,
    especialmente con montajes bind donde el mapeo de UID/GID importa.
  7. El etiquetado SELinux ha sido un obstáculo frecuente en migraciones. Mover datos montados por bind entre hosts con contextos SELinux distintos
    puede romper cargas de trabajo incluso si los bytes son correctos.
  8. Kubernetes normalizó la idea de volúmenes persistentes explícitos con drivers y claims. Los volúmenes nombrados de Docker se parecen,
    pero no son la misma historia ni nivel de abstracción para migraciones.

Matriz de supervivencia en migraciones: montajes bind vs volúmenes nombrados

Lo que “sobrevive a las migraciones” realmente significa

“Sobrevivir” implica mucho. Una migración tiene éxito cuando:

  • Los datos llegan intactos (sumas de comprobación coinciden, no solo “arranca”).
  • Los permisos y la propiedad son correctos para el modelo de runtime del contenedor.
  • Las características de rendimiento no son catastróficamente diferentes (IOPS, latencia de fsync, comportamiento del cache de páginas).
  • Los flujos operativos siguen funcionando (backups, restauraciones, diagnóstico de incidentes).

Montajes bind: fortalezas y modos de fallo

Fortalezas:

  • Migración directa: transferible con herramientas estándar.
  • Transparente: puedes ver los datos sin que Docker esté en ejecución.
  • Compatible con funciones de almacenamiento a nivel de host: snapshots, replicación, cuotas, cifrado, dedupe, compresión.
  • Mejor para cumplimiento: cuando los auditores preguntan “¿dónde están los datos?” puedes responder sin una clase sobre Docker.

Modos de fallo:

  • Acoplamiento a la ruta: tu stack asume que /srv/app/db existe en cada host.
    Si reemplazas un host y olvidas ese directorio (o su montaje), el contenedor arranca sobre un directorio vacío. Enhorabuena, inventaste pérdida de datos.
  • Desajuste de permisos: el usuario del contenedor espera UID 999 pero tus archivos restaurados son propiedad de 1001.
    “Permiso denegado” es la versión educada de esto.
  • Fricción SELinux/AppArmor: los bytes están presentes pero el acceso está bloqueado. Esto es el comportamiento correcto más molesto en Linux.
  • Exposición accidental del host: un bind mount de / o de /var/run/docker.sock es un evento de seguridad esperando suceder.

Volúmenes nombrados: fortalezas y modos de fallo

Fortalezas:

  • Portabilidad de la configuración: los archivos Compose no codifican rutas del host.
  • Más seguros por defecto: menos desastres de “ups monté el directorio equivocado”.
  • Aislamiento: los datos están bajo la gestión de Docker, reduciendo la manipulación casual.
  • Flexibilidad del driver: puedes usar plugins para apuntar a NFS, dispositivos en bloque o almacenamiento en la nube (pero ahora también operas un driver).

Modos de fallo:

  • La migración requiere pasos conscientes de Docker. Si tratas los datos del volumen como un blob opaco bajo /var/lib/docker,
    heredas toda la complejidad interna de Docker.
  • Dependencia oculta en la configuración del motor: rootless, VM de Desktop, driver de almacenamiento, elección del sistema de archivos.
  • Puntos ciegos en backups: las herramientas de respaldo de infraestructura a menudo excluyen /var/lib/docker porque se considera “reconstruible.”
    Tus volúmenes nombrados pueden vivir en la zona excluida.

Entonces, ¿qué sobrevive mejor?

En las migraciones, lo explícito vence a lo implícito. Los montajes bind son explícitos: puedes verlos y moverlos.
Los volúmenes nombrados pueden sobrevivir perfectamente si operacionalizas exportaciones y restauraciones y no dependes de rutas no documentadas.
La mayoría de los equipos no ensayan eso. Ensayan “re-desplegar contenedores”, no “reubicar estado.”

Tareas prácticas (comandos, salidas y decisiones)

Esta es la parte donde dejas de adivinar. Cada tarea incluye: un comando, qué significa la salida y qué decisión tomar.
Ejecútalas en el host origen antes de la migración y otra vez en el host destino después.

Tarea 1: Listar contenedores y sus montajes (encuentra lo que realmente tiene estado)

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Mounts}}'
NAMES          IMAGE                 MOUNTS
pg01           postgres:16           pgdata
api01          myorg/api:2.4.1       /srv/api/config:/app/config
grafana01      grafana/grafana:11    grafana-storage

Significado: la columna mounts mezcla volúmenes nombrados (por ejemplo, pgdata) y montajes bind (las rutas del host aparecen como host:container).

Decisión: todo lo que contenga estado de negocio necesita un plan de migración. “Es solo caché” no es un plan.

Tarea 2: Inspeccionar los montajes de un contenedor (la verdad, no las sensaciones)

cr0x@server:~$ docker inspect pg01 --format '{{json .Mounts}}'
[{"Type":"volume","Name":"pgdata","Source":"/var/lib/docker/volumes/pgdata/_data","Destination":"/var/lib/postgresql/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]

Significado: Type te dice bind vs volume. Source para un volumen nombrado local a menudo apunta bajo el directorio de datos de Docker.

Decisión: si es un volumen nombrado, planifica export/import. Si es un montaje bind, planifica la migración del sistema de archivos y comprobaciones de permisos.

Tarea 3: Inspeccionar un volumen nombrado (driver, punto de montaje, etiquetas)

cr0x@server:~$ docker volume inspect pgdata
[
  {
    "CreatedAt": "2025-11-02T09:14:21Z",
    "Driver": "local",
    "Labels": {
      "com.docker.compose.project": "billing",
      "com.docker.compose.volume": "pgdata"
    },
    "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
    "Name": "pgdata",
    "Options": null,
    "Scope": "local"
  }
]

Significado: Scope: local significa “esto no se comparte mágicamente.” Mountpoint es donde viven los bytes en este host.

Decisión: si esperabas almacenamiento HA, no lo tienes. Si esperabas “migración automática”, tampoco la tienes.

Tarea 4: Confirmar que la ruta del host detrás de un bind mount exista y esté montada correctamente

cr0x@server:~$ ls -ld /srv/api/config
drwxr-x--- 2 root api 4096 Jan  2 09:10 /srv/api/config

cr0x@server:~$ findmnt -T /srv/api/config
TARGET SOURCE          FSTYPE OPTIONS
/srv   /dev/nvme0n1p2  ext4   rw,relatime

Significado: los datos del montaje bind viven en el sistema de archivos que respalda esa ruta; aquí es ext4 en una partición NVMe local.

Decisión: si findmnt muestra la fuente equivocada (o nada), arregla los montajes antes de migrar. Si no, migrarás el directorio marcador vacío.

Tarea 5: Comprobar espacio libre e inodos (las migraciones fallan porque te quedaste sin “no-espacio”)

cr0x@server:~$ df -h /srv /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  200G  120G   71G  63% /srv
/dev/nvme0n1p3  100G   92G  3.5G  97% /var/lib/docker

cr0x@server:~$ df -i /var/lib/docker
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/nvme0n1p3  655360 621104  34256   95% /var/lib/docker

Significado: la partición de datos de Docker está casi llena y escasa de inodos. Es una configuración clásica de “las cosas se ponen raras”.

Decisión: no migrar un desastre. Arregla la capacidad primero o cambia dónde Docker guarda datos.

Tarea 6: Identificar la raíz de datos de Docker (no asumas /var/lib/docker)

cr0x@server:~$ docker info --format '{{.DockerRootDir}}'
/var/lib/docker

Significado: aquí es donde Docker almacena imágenes, capas y volúmenes locales nombrados (a menos que un driver cambie eso).

Decisión: para volúmenes nombrados, exportarás desde aquí; para montajes bind, esto es mayormente irrelevante salvo estrategias de migración del motor.

Tarea 7: Encontrar qué volúmenes se usan vs cuáles están huérfanos (reducir el alcance de la migración)

cr0x@server:~$ docker volume ls
DRIVER    VOLUME NAME
local     billing_pgdata
local     grafana-storage
local     old_tmpdata

cr0x@server:~$ docker ps -a --format '{{.Names}}' | wc -l
7

Significado: la lista de volúmenes incluye volúmenes huérfanos. El conteo de contenedores te dice cuántas cargas podrían referenciarlos.

Decisión: identifica huérfanos antes de migrar; si no, llevarás bytes muertos como si fueran reliquias de familia que nadie quiso.

Tarea 8: Mapear un volumen nombrado a los contenedores que lo usan

cr0x@server:~$ docker ps -a --filter volume=billing_pgdata --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
NAMES   STATUS         IMAGE
pg01    Up 3 hours     postgres:16

Significado: solo pg01 usa este volumen.

Decisión: el plan de migración puede ser por volumen; no necesitas exportar todo “por si acaso.”

Tarea 9: Exportar un volumen nombrado a un tarball (portátil y aburrido)

cr0x@server:~$ docker run --rm -v billing_pgdata:/data -v /backup:/backup alpine:3.20 sh -c 'cd /data && tar -cpf /backup/billing_pgdata.tar .'
cr0x@server:~$ ls -lh /backup/billing_pgdata.tar
-rw-r--r-- 1 root root 2.1G Jan  2 10:01 /backup/billing_pgdata.tar

Significado: tienes un único artefacto que representa el contenido del volumen.

Decisión: este es el valor por defecto seguro para migraciones de volúmenes nombrados. Si el tarball es enorme, considera tiempo de inactividad frente a estrategias incrementales.

Tarea 10: Importar un tarball de volumen nombrado en el host nuevo (y verificar que no esté vacío)

cr0x@server:~$ docker volume create billing_pgdata
billing_pgdata

cr0x@server:~$ docker run --rm -v billing_pgdata:/data -v /backup:/backup alpine:3.20 sh -c 'cd /data && tar -xpf /backup/billing_pgdata.tar'

cr0x@server:~$ docker run --rm -v billing_pgdata:/data alpine:3.20 sh -c 'ls -la /data | head'
total 72
drwx------    19 999      999           4096 Jan  2 10:05 .
drwxr-xr-x     1 root     root          4096 Jan  2 10:05 ..
drwx------     5 999      999           4096 Jan  2 10:05 base
-rw-------     1 999      999             88 Jan  2 10:05 postmaster.opts

Significado: los archivos existen y la propiedad se parece a la típica de Postgres (UID 999 aquí).

Decisión: si la propiedad es incorrecta, arréglala antes de arrancar Postgres, no después de que falle a mitad de la recuperación.

Tarea 11: Migrar un bind mount con rsync (preserva propiedad, xattrs, ACLs)

cr0x@server:~$ sudo rsync -aHAX --numeric-ids --delete /srv/api/config/ cr0x@newhost:/srv/api/config/
sending incremental file list
./
app.yaml
secrets.env

sent 24,198 bytes  received 91 bytes  48,578.00 bytes/sec
total size is 18,422  speedup is 0.76

Significado: -aHAX conserva metadatos; --numeric-ids evita desajustes de nombres de usuario entre hosts.

Decisión: si rsync reporta eliminaciones masivas inesperadas, detente y confirma que no apuntaste a la ruta equivocada.

Tarea 12: Verificar sumas de comprobación para un directorio montado por bind (integridad, no optimismo)

cr0x@server:~$ cd /srv/api/config && sudo find . -type f -maxdepth 2 -print0 | sort -z | xargs -0 sha256sum > /tmp/api-config.sha256
cr0x@server:~$ head /tmp/api-config.sha256
b2e58d0c5c2de0c8c61b49e0b8d8c0c95bdb9b5c5b718e3cc7c4f2c1f8f91ac4  ./app.yaml
c1aa9b3e0c6c4e99c7b2e5fd3d9b2a7a1bb6c32d08d5f9d7c7089a8d77c0e0a2  ./secrets.env

Significado: un manifiesto de sumas de comprobación. Ejecuta el mismo en el destino y compara.

Decisión: para configuración/estado crítico, las comparaciones de suma de comprobación superan a “parece bien” todos los días.

Tarea 13: Detectar desajustes UID/GID que romperán contenedores tras la migración

cr0x@server:~$ stat -c '%n %u:%g %a' /srv/api/config
/srv/api/config 0:1002 750

cr0x@server:~$ getent group api
api:x:1002:

Significado: el directorio es de grupo GID 1002. Si el host nuevo asigna api un GID distinto, el acceso de grupo falla.

Decisión: estandariza la asignación de UID/GID del sistema (o usa IDs numéricos de forma consistente) para rutas montadas por bind.

Tarea 14: Comprobar modo y contexto SELinux (las migraciones de bind mount adoran fallar aquí)

cr0x@server:~$ getenforce
Enforcing

cr0x@server:~$ ls -Zd /srv/api/config
unconfined_u:object_r:default_t:s0 /srv/api/config

Significado: SELinux está en modo enforcing y el directorio tiene default_t, que a menudo bloquea el acceso desde contenedores.

Decisión: usa etiquetas correctas (o opciones de montaje en la definición del contenedor). Migrar datos sin restaurar los contextos adecuados produce “permiso denegado” con pasos extra.

Tarea 15: Observar síntomas de latencia IO en vivo (¿la “migración” es en realidad una regresión de almacenamiento?)

cr0x@server:~$ iostat -x 1 3
Linux 6.8.0 (server) 	01/02/2026 	_x86_64_	(8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           7.42    0.00    2.13    9.77    0.00   80.68

Device            r/s     w/s   rMB/s   wMB/s avgrq-sz avgqu-sz await r_await w_await  svctm  %util
nvme0n1         21.0   180.0    1.2    14.8    177.4     2.10  11.6    2.1   12.7   0.45  90.4

Significado: await ~11ms y %util ~90% sugiere que el disco está ocupado y la latencia no es trivial.

Decisión: si el host destino muestra peor latencia, tu migración “funcionó” pero tu base de datos se quejará fuerte. Arregla la clase de almacenamiento antes de culpar a Docker.

Tarea 16: Confirmar qué tipo de montaje usa un contenedor en ejecución (comprobación tras el corte)

cr0x@server:~$ docker inspect api01 --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
bind /srv/api/config -> /app/config

Significado: tras la migración, confirmas que el contenedor usa el tipo de montaje y la ruta prevista.

Decisión: si esperabas un montaje bind y ves un volumen, has cambiado accidentalmente la semántica. Detén y arregla antes de acumular estado divergente.

Guion de diagnóstico rápido

Ya migraste. Algo está lento, roto, o “funciona en un host y no en el otro.” Empieza aquí. Esta secuencia está diseñada para encontrar el cuello de botella rápidamente,
no para satisfacer tu curiosidad sobre cada capa.

1) Confirma que estás montando lo que crees que montas

  • Ejecuta docker inspect <container> y revisa .Mounts por Type, Source y Destination.
    Un número sorprendente de incidentes son “se montó un directorio vacío.”
  • Para montajes bind, ejecuta findmnt -T /host/path para asegurarte de que está respaldado por el sistema de archivos/dispositivo previsto.

2) Revisa permisos y etiquetas de seguridad

  • Compara la salida de stat en el origen vs el destino. La deriva de UID/GID es común tras reconstrucciones.
  • Si SELinux está en modo enforcing, comprueba contextos con ls -Z. Si AppArmor está en juego, revisa el confinamiento del perfil (a menudo visible en logs del contenedor).

3) Revisa la salud del almacenamiento antes de afinar la aplicación

  • Observa latencia de dispositivo (iostat -x) y saturación del sistema de archivos (df -h, df -i).
  • Si es una base de datos y ves timeouts, asume latencia de fsync hasta que se pruebe lo contrario.

4) Solo entonces inspecciona capas específicas de Docker

  • Confirma el directorio raíz de Docker (docker info), el driver de almacenamiento y si se está usando modo rootless.
  • Si copiaste el directorio de datos de Docker entre hosts, detente y verifica compatibilidad de versión del motor y del driver de almacenamiento antes de profundizar.

Una idea parafraseada atribuida a Werner Vogels: “Todo falla, todo el tiempo; diseña y opera como si eso fuera verdad.”

Tres mini-historias corporativas (cómo la gente realmente sale perjudicada)

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

Una empresa mediana ejecutaba un servicio de facturación con Postgres en Docker. El archivo Compose usaba un volumen nombrado: billing_pgdata.
El modelo mental del equipo era: “Volumen nombrado equivale a persistente, persistente equivale a que sobrevive al reemplazo del host.”
Nadie documentó dónde vivían los bytes. Nadie lo necesitó. Hasta que lo necesitaron.

Una actualización rutinaria del SO se convirtió en “reconstruyamos la instancia.” El equipo de infraestructura terminó la VM antigua después de que la nueva pasó las comprobaciones de salud.
Los contenedores arrancaron inmediatamente. Postgres arrancó inmediatamente también—en un volumen nuevo y vacío creado en el host nuevo. La aplicación también arrancó y empezó
a crear filas en una base de datos totalmente nueva que parecía perfectamente sana. Las alertas se mantuvieron verdes porque el uptime era verde.

El primer humano lo notó cuando finanzas preguntó por qué faltaban las facturas de ayer. Para entonces, los datos del volumen local del host antiguo se habían ido.
“Pero era un volumen” resultó ser una afirmación sobre la API de Docker, no sobre el ciclo de vida de la infraestructura.

La solución no fue heroica. Reconstruyeron desde backups (que existían, por suerte) y escribieron un runbook de migración que trataba los volúmenes nombrados
como artefactos exportables. También añadieron una comprobación canaria: la app se niega a arrancar si no está presente una huella del esquema.
El incidente fue una lección sobre suposiciones. Los datos no desaparecieron. Su entendimiento sí.

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

Otra organización quería “archivos Compose más limpios” y menos rutas específicas del host. Reemplazaron montajes bind por volúmenes nombrados en todas partes,
incluyendo un servicio con mucha escritura que usaba SQLite (sí, en producción; sí, sucede).
Su razonamiento: los volúmenes nombrados reducen errores de ruta y “Docker lo gestiona mejor.”

El rendimiento se desplomó tras el cambio—no porque los volúmenes nombrados sean intrínsecamente lentos, sino por dónde acabaron.
El directorio raíz de Docker vivía en una partición más pequeña respaldada por almacenamiento en red optimizado para throughput, no para latencia.
El patrón fsync de SQLite castigó ese almacenamiento. La app no se cayó; simplemente se volvió más lenta de formas que parecían agotamiento de CPU y
“quizá necesitamos más pods”, aunque no había pods involucrados.

El equipo persiguió fantasmas: optimizaron PRAGMAs, añadieron caches y consideraron cambiar el ORM. El problema real fue aburrido:
movieron IO con estado desde un disco local rápido (donde vivía el bind mount) a una capa de almacenamiento de alta latencia (donde vivía /var/lib/docker).
Mismos bytes. Física diferente.

El rollback fue igualmente soso: devolvieron la base de datos a un bind mount apuntando al disco rápido, mantuvieron volúmenes nombrados para datos menos sensibles
y trataron la ubicación raíz de Docker como una decisión de capacidad/rendimiento de primera clase. La optimización no era errónea en concepto.
Era errónea en contexto. El contexto es donde viven los incidentes.

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

Una empresa global ejecutaba varios servicios internos en hosts Docker que eran reemplazados frecuentemente por una canalización de automatización.
Nada sofisticado. La parte aburrida: cada host tenía un layout de puntos de montaje dedicado para datos con estado:
/srv/state/<service>, con un sistema de archivos por cada componente estatal principal. Cada sistema tenía snapshots y políticas de backup.

Sus contenedores usaban montajes bind exclusivamente para estado. Los archivos Compose no eran “portables” en el sentido de que no podías simplemente ejecutarlos
en tu laptop sin crear directorios. Pero el entorno de producción era consistente, y la consistencia es básicamente disponibilidad con gabardina.

Cuando un host sufrió corrupción en el sistema de archivos (sucede), la respuesta fue casi aburrida:
marcar el host, restaurar el sistema de archivos afectado desde el último snapshot, adjuntarlo a un host de reemplazo, reiniciar contenedores.
El informe del incidente fue corto porque el diseño del sistema ya asumía el reemplazo.

Su arma secreta no fue una herramienta. Fue una práctica: cada trimestre ensayaban restaurar el estado de un servicio en un host nuevo.
No porque esperaran que ocurriera. Porque esperaban que no ocurriera, y no confiaban en ese sentimiento.

Chiste #2: Lo único más permanente que un contenedor Docker es la solución temporal que usaste para migrar sus datos.

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

1) “Arrancó bien, pero todos los datos se perdieron”

Síntoma: el servicio arranca con base de datos vacía o configuración por defecto después de la migración.

Causa raíz: la ruta del montaje bind no existía o no estaba montada; Docker creó un directorio vacío y lo montó.
Con volúmenes nombrados, creaste un volumen nuevo en lugar de importar los datos antiguos.

Solución: verifica montajes del host con findmnt; añade comprobaciones de arranque (huella del esquema, archivos esperados).
Fija nombres de volúmenes en Compose; exporta/importa volúmenes explícitamente.

2) “Permiso denegado” dentro del contenedor tras mover datos

Síntoma: logs de la aplicación muestran fallos para leer/escribir rutas montadas; Postgres se queja de la propiedad del directorio de datos.

Causa raíz: desajuste UID/GID (especialmente entre distribuciones), diferencias en user namespaces/mode rootless, o ACLs/xattrs faltantes.

Solución: migra con rsync -aHAX --numeric-ids; estandariza UIDs/GIDs; considera ejecutar contenedores con user: explícito.
Para SELinux, aplica contextos correctos o usa las opciones de etiquetado de montaje apropiadas.

3) “Funciona en host Ubuntu, falla en host RHEL”

Síntoma: rutas bind-mounted no legibles; contenedores no pueden acceder a archivos que existen.

Causa raíz: SELinux en modo enforcing con etiquetas incorrectas en el directorio bind-mounted.

Solución: aplica etiquetas SELinux correctas al directorio o usa las opciones de montaje y políticas de contenedor adecuadas.
Valida con ls -Z antes de declarar que Docker “está roto.”

4) “La migración tuvo éxito, pero el rendimiento es terrible”

Síntoma: mayor latencia, timeouts, queries lentas, alto IOwait.

Causa raíz: los datos del volumen se movieron a una capa de almacenamiento más lenta (a menudo porque el directorio raíz de Docker está en un disco distinto que antes),
o las opciones del sistema de archivos difieren (barriers, modo de journal, atime).

Solución: confirma dispositivo y sistema de archivos con findmnt, iostat -x, df.
Pon los bind mounts estatales en el disco previsto. Si usas volúmenes nombrados, reubica Docker root a un sistema de archivos adecuado o usa un driver de volumen apropiado.

5) “Copiamos /var/lib/docker y ahora Docker no arranca / contenedores están corruptos”

Síntoma: errores del daemon Docker, capas faltantes, problemas con el filesystem overlay.

Causa raíz: desajuste de versión del motor, driver de almacenamiento distinto, incompatibilidad de sistema de archivos, o copia parcial sin xattrs.

Solución: evita migrar todo el root de datos de Docker como estrategia salvo que lo hayas probado en ese entorno exacto.
Exporta/importa volúmenes nombrados y reconstruye imágenes desde registries. Para recuperación de emergencia, empareja versiones y copia preservando metadatos.

6) “Hay backups, pero la restauración no funciona”

Síntoma: los backups restauran archivos, pero el servicio no arranca o los datos están inconsistentes.

Causa raíz: hiciste backup de un directorio de base de datos en caliente sin snapshots consistentes con la aplicación; o restauraste sin permisos/xattrs.

Solución: usa backups nativos de la base de datos (o snapshots coordinados). Para backups a nivel de sistema de archivos, pausa el servicio o usa features de snapshot correctamente.

Listas de verificación / plan paso a paso

Plan A: Si quieres que las migraciones sean aburridas (recomendado)

  1. Inventario de montajes: ejecuta docker ps y docker inspect para listar rutas y volúmenes con estado.
  2. Clasifica los datos: base de datos, uploads de usuarios, caché, artefactos de build, configuración, secretos. Solo parte de esto merece dolor.
  3. Elige la propiedad del almacenamiento:
    • Estado crítico: montaje bind a /srv/state/<service> en un sistema de archivos dedicado.
    • Estado reemplazable: los volúmenes nombrados son aceptables.
  4. Estandariza el layout del host: mismos puntos de montaje entre hosts, preferiblemente aprovisionados por automatización.
  5. Estandariza identidad: asegura que cuentas de servicio e IDs numéricos no variarán entre hosts.
  6. Respalda lo correcto: sistemas de archivos montados con bind mediante tu sistema de backups normal; volúmenes nombrados mediante jobs periódicos de export (tar a almacenamiento de backup).
  7. Ensaya la restauración: una vez por trimestre, elige un servicio y restáuralo en un host fresco. Mide tiempo hasta usable.
  8. Añade salvaguardas: la app se niega a arrancar si falta la firma de datos esperada; alerta sobre eventos de “inicio desde cero”.

Plan B: Si ya usaste volúmenes nombrados por todas partes

  1. Enumera volúmenes nombrados: docker volume ls y mapea a contenedores.
  2. Exporta volúmenes: tar cada volumen nombrado a una ruta de backup con un esquema de nombres consistente.
  3. Almacena con integridad: calcula sumas de comprobación de los tarballs y guárdalas junto a ellos.
  4. Importa en el destino: crea volúmenes primero, luego extrae los tarballs dentro de ellos.
  5. Verifica propiedad y contenido: lista directorios clave dentro del volumen vía un contenedor temporal.
  6. Corte: arranca servicios, ejecuta pruebas simples y valida presencia de datos (no solo endpoints de salud).

Plan C: Si necesitas el “reemplazo más rápido posible” del host

Aquí pagas por decisiones previas. Los reemplazos rápidos ocurren cuando el estado vive en almacenamiento que puede desacoplarse/adjuntarse o replicarse
independientemente del host de cómputo.

  1. Pon el estado en una unidad desacoplable: disco separado, dataset o volumen de red.
  2. Montaje bind de esa unidad: los contenedores montan rutas /srv/state que apuntan a esa unidad.
  3. Automatiza la attach: el host de reemplazo arranca, adjunta el almacenamiento, lo monta y luego arranca contenedores.
  4. Prueba la falla: simula pérdida de host y mide tiempo de recuperación. Si no lo mides, no lo tienes.

Preguntas frecuentes (FAQ)

1) ¿Los volúmenes nombrados son “más seguros” que los montajes bind?

Son más seguros contra ciertos errores humanos (ruta equivocada, montar / accidentalmente), pero no son inherentemente más seguros en durabilidad.
La durabilidad viene del almacenamiento subyacente y de tu disciplina de backup/restore.

2) ¿Puedo simplemente copiar /var/lib/docker/volumes para migrar volúmenes nombrados?

A veces. Suficiente gente lo ha hecho como para que suene normal. Pero está acoplado a versión del motor, driver de almacenamiento y detalles del sistema de archivos.
Si necesitas una migración repetible, exporta/importa con tar (o usa un driver que proporcione portabilidad).

3) ¿Por qué los montajes bind “sobreviven mejor” si están ligados a rutas del host?

Porque la ruta es tu contrato y puedes imponerla con automatización. Un montaje bind es simplemente datos normales en un sistema de archivos normal.
Eso encaja bien con herramientas maduras de backup/replicación. Los volúmenes nombrados también son archivos normales, pero están ocultos bajo la gestión y ubicación de Docker.

4) ¿Y Docker Desktop en macOS/Windows?

El motor se ejecuta dentro de una VM. Los volúmenes nombrados viven en esa VM. Los montajes bind cruzan la frontera de la VM y tienen sus propias peculiaridades de rendimiento y semántica.
Para migraciones, trata los volúmenes de Desktop como “locales a esa máquina” a menos que los exportes deliberadamente.

5) ¿Cuál es más rápido: montajes bind o volúmenes nombrados?

En Linux con almacenamiento local, el rendimiento suele ser similar porque ambos terminan como IO de sistema de archivos. Las diferencias reales son:
dónde residen físicamente los datos, opciones de montaje y capas de seguridad. Mide en tu hardware; no confíes en folklore.

6) ¿Los volúmenes nombrados ayudan con permisos?

Pueden, porque Docker crea y posee el directorio bajo su root y a menudo lo mantiene consistente. Pero los permisos siguen importando dentro del volumen.
Si tu contenedor corre como no-root, aún necesitas la propiedad correcta.

7) ¿Cuál es la mejor práctica para bases de datos en Docker?

Pon la base de datos en un almacenamiento que puedas respaldar y restaurar de forma fiable. En muchas organizaciones eso significa un montaje bind a un sistema de archivos dedicado o dispositivo de bloque gestionado,
más herramientas de backup con consistencia de aplicación. Los volúmenes nombrados son aceptables si tu export/import y backups son igual de disciplinados.

8) ¿Cómo prevengo desastres de “directorio vacío montado” con montajes bind?

Crea el directorio y móntalo mediante automatización. Añade una comprobación previa al arranque: si el directorio no contiene un archivo marcador esperado, falla rápido.
Y valida montajes del host con findmnt durante el despliegue.

9) Si uso Docker Compose, ¿debo preferir volúmenes nombrados por portabilidad?

Prefiere la portabilidad solo si no sabotea la recuperabilidad. Para desarrollo: los volúmenes nombrados son estupendos. Para estado en producción: usa montajes bind a rutas estables del host,
o acepta volúmenes nombrados pero trata export/import como parte de tu ciclo de despliegue.

10) ¿Y los drivers de volumen (NFS, cloud, etc.)?

Pueden resolver migraciones poniendo datos en almacenamiento compartido o desacoplable, pero añaden un nuevo dominio de fallo: el driver y el servicio de red/almacenamiento.
Úsalos cuando necesites estado compartido o reubicación rápida, y prueba su comportamiento ante fallos bajo carga.

Próximos pasos prácticos

  1. Haz una auditoría hoy: lista todos los montajes y clasifica cuáles son estado de negocio. Si no puedes nombrarlo, no puedes protegerlo.
  2. Elige un camino de migración y estandarízalo:
    montajes bind en un layout consistente /srv/state, o volúmenes nombrados con exportaciones scripts y verificación de sumas.
  3. Mueve intencionalmente la raíz de Docker: si los volúmenes nombrados te importan, el sistema de archivos bajo DockerRootDir es almacenamiento de producción.
    Trátalo como tal.
  4. Escribe el runbook de “restaurar bajo presión”: incluye comandos exactos para exportar/importar, arreglar propiedad, validar presencia de datos y verificar rendimiento.
  5. Ensaya: realiza una restauración completa en un host nuevo. Mídelo. Arregla lo que duela. Repite hasta que sea aburrido.

Los montajes bind no son mágicos. Los volúmenes nombrados no son malos. La diferencia es si tu plan de migración depende de internos de Docker o de fundamentos del sistema de archivos.
Si quieres que las migraciones sean rutinarias, apóyate en los fundamentos.

← Anterior
Mapeo de ACL de ZFS para SMB: evitar pesadillas de permisos
Siguiente →
MySQL vs PostgreSQL en un VPS de 1GB RAM: qué es realmente usable (y las configuraciones que lo hacen)

Deja un comentario