La migración de almacenamiento que duele no es la que falla ruidosamente. Es la que “funcionó” y silenciosamente eliminó algunos archivos, algunos permisos o unos milisegundos de durabilidad hasta que tu base de datos aprende a qué sabe la entropía.
Los volúmenes Docker deberían ser aburridos. Luego los mueves: nuevo disco, nuevo host, nuevo driver, nuevo nombre de stack en Compose, o “solo una reconstrucción rápida”. Ahí es donde ocurre la pérdida de datos—normalmente porque tratamos un volumen como una carpeta, o tratamos una carpeta como un volumen.
El truco: migrar mediante un montaje controlado en un contenedor (no el sistema de ficheros del host)
Aquí está la maniobra que evita la mayoría de los desastres de migración de volúmenes: no copies “lo que esté bajo
/var/lib/docker” y no confíes en que una ruta de bind mount sea igual que un volumen nombrado. En su lugar, adjunta el volumen antiguo y el destino nuevo (un volumen nuevo, un bind mount o una tubería SSH) a un contenedor utilitario de un solo uso y copia dentro de ese entorno controlado.
Por qué funciona: el montaje desde el contenedor te da una vista estable del contenido del volumen exactamente como lo ve tu aplicación. No atraviesas accidentalmente los metadatos internos de Docker. No pierdes archivos porque copiaste el directorio equivocado. Y puedes estandarizar el proceso con comandos repetibles.
Mi predeterminado es un enfoque en dos fases:
- Fase 1 (copia previa): mantener la aplicación en ejecución, hacer una sincronización inicial para reducir el tiempo de inactividad.
- Fase 2 (copia de corte): detener la aplicación (o al menos silenciarla), luego hacer una sincronización final y verificar.
Si solo te llevas una cosa de este texto: trata la migración como un despliegue. Necesitas un plan, una reversión, verificación y un corte limpio.
Hechos interesantes e historia corta que explican las trampas actuales
- Los volúmenes Docker existían antes que Docker Compose. Los volúmenes fueron el primer mecanismo oficial de persistencia; Compose después los hizo fáciles de declarar y aún más fáciles de malinterpretar.
- Los volúmenes nombrados son objetos gestionados, no rutas. Se mapean a rutas en el host, pero Docker se reserva el derecho de elegir dónde y cómo. Copiar “la carpeta” a menudo copia la carpeta equivocada.
-
Los sistemas de ficheros overlay no son donde almacenas estado.
overlay2de Docker es fantástico para capas de imagen y escrituras efímeras de contenedores. No es el lugar feliz de tu base de datos. - Los volúmenes sobreviven a los contenedores por diseño. El borrado de un contenedor no elimina volúmenes nombrados a menos que lo hagas explícitamente. Esto es una característica de fiabilidad—y una trampa operativa cuando los volúmenes antiguos se acumulan.
- Los desajustes de permisos empeoraron con Docker sin root. Los motores rootless cambian los mapeos UID/GID; la migración puede tener éxito y aun así fallar en tiempo de ejecución con “permission denied”.
- macOS/Windows Desktop tiene una capa de virtualización. En Desktop, los volúmenes viven dentro de una VM. “Copiar desde el host” no es lo que crees, y las características de rendimiento difieren drásticamente.
-
Los drivers de almacenamiento han cambiado comportamientos por defecto entre versiones. Lo que fue
aufspasó a seroverlay2para la mayoría de distribuciones Linux; la semántica de volúmenes se mantuvo estable, pero “dónde están los datos” sigue confundiendo a la gente. -
Algunos motores de base de datos tratan las marcas de tiempo como parte de la corrección. Si tu migración cambia
mtimede formas extrañas (o restauras con opciones incorrectas), ciertas cargas de trabajo se vuelven más lentas o se comportan de forma errática. - Los sistemas de ficheros en red siguen siendo un campo minado de compatibilidad. Las opciones de NFS, el bloqueo y el comportamiento de fsync pueden convertir una migración “exitosa” en una corrupción sutil posterior.
Modelo mental: qué es (y qué no es) un volumen Docker
Volumen nombrado vs bind mount vs “cosas dentro del contenedor”
Tres categorías de almacenamiento aparecen en incidentes reales:
-
Volumen nombrado: almacenamiento persistente gestionado por Docker. El motor Docker lo crea y lo monta en tu contenedor.
Normalmente vive bajo/var/lib/docker/volumes/<name>/_dataen Linux, pero no construyas automatizaciones que asuman esta ruta. - Bind mount: una ruta del host montada en un contenedor. Genial para desarrollo y para algunos casos de producción donde controlas la disposición de rutas, backups y permisos.
- Capa escribible del contenedor: la “diferencia” sobre la imagen. Muere con el contenedor. Si tienes datos de negocio ahí, no tienes persistencia—tienes sensaciones.
Por qué copiar “/var/lib/docker” es un hobby peligroso
Docker almacena imágenes, capas, caché de compilación, redes, metadatos de contenedores y volúmenes bajo su data-root. Copiarlo mientras Docker está en ejecución puede producir un árbol de directorios que aparenta coherencia pero que es internamente inconsistente. Solo lo descubrirás cuando los contenedores se nieguen a arrancar, o peor, arranquen con estado parcial.
Si debes trasladar todo el data-root de Docker, hazlo como un procedimiento quirúrgico: detén Docker, copia, verifica, actualiza data-root, luego arranca Docker. Pero eso no es “migración de volúmenes”. Eso es “mover las entrañas del motor”.
El principio de fiabilidad que importa
Cuando migras estado, estás moviendo un contrato: contenido, permisos, propiedad, atributos extendidos, hardlinks, symlinks y a veces comportamiento de archivos dispersos. Tu herramienta necesita preservar el contrato.
Una cita que la gente de operaciones vuelve a aprender:
Idea parafraseada — Richard Cook (seguridad de sistemas): “El éxito oculta la complejidad del sistema; las fallas la revelan.”
Los chistes suelen ser una mala estrategia de fiabilidad, pero aquí va uno de todas formas: una migración de volumen Docker es como mover una pecera—puedes hacerlo rápido, o puedes hacerlo dos veces.
Guía de diagnóstico rápido: encuentra el cuello de botella en minutos
Cuando una migración es lenta o arriesgada, no necesitas una semana de benchmarks. Necesitas un triage rápido y disciplinado.
Revisa esto en orden:
1) ¿Estás limitado por I/O en la fuente, el destino o la red?
- Ejecuta
iostat/vmstaten ambos extremos. - Observa await alto, util alto, bajo rendimiento y paginación.
- Si copias por SSH, revisa también la CPU; el cifrado puede ser el cuello de botella.
2) ¿Estás copiando accidentalmente lo incorrecto (o demasiado)?
- Confirma el/los volumen(es) y los puntos de montaje con
docker volume inspectydocker inspect. - Lista lo que realmente hay dentro del volumen usando un contenedor utilitario.
- Mide el tamaño con
du -xdesde dentro del montaje.
3) ¿Tratas con una aplicación que debe ser silenciada?
- Bases de datos, colas y cualquier cosa con write-ahead logs necesita un momento de parada total (o snapshot).
- Si no puedes detenerla, tu “migración” es un proyecto de replicación. No finjas lo contrario.
4) ¿Los permisos/propiedad te van a afectar después del corte?
- Revisa UID/GID dentro del contenedor en ejecución.
- Comprueba si estás ejecutando Docker sin root o con espacios de nombres de usuarios.
- Espera diferencias de SELinux/AppArmor entre hosts.
5) ¿Estás verificando el resultado con más que esperanza?
- Cuenta archivos, compara checksums de una muestra y valida la salud a nivel de aplicación (no solo que el contenedor esté “Up”).
- Mantén el volumen antiguo intacto hasta que hayas completado un ciclo de negocio completo.
Tareas prácticas: comandos, salidas y qué decidir a partir de ellas
Estas son tareas reales de producción. Cada una incluye: el comando, cómo se ve una salida “normal” y qué decisión tomas.
Úsalas como bloques de construcción para tus propios runbooks.
Tarea 1: Identificar si usas un volumen nombrado o un bind mount
cr0x@server:~$ docker inspect -f '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}' app
volume /var/lib/docker/volumes/pgdata/_data -> /var/lib/postgresql/data
bind /srv/app/config -> /etc/app
Qué significa: Tienes un volumen nombrado (pgdata) y un bind mount (/srv/app/config).
Decisión: migra pgdata mediante copia de volumen a volumen; migra los bind mounts mediante copia normal del sistema de ficheros con reglas de propiedad explícitas.
Tarea 2: Inspeccionar metadata del volumen (driver, mountpoint)
cr0x@server:~$ docker volume inspect pgdata
[
{
"CreatedAt": "2026-01-20T10:22:14Z",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
"Name": "pgdata",
"Options": {},
"Scope": "local"
}
]
Qué significa: driver local, alcance local del host. No es un volumen de clúster.
Decisión: planea una copia a nivel de host (tar/rsync) o usa un contenedor intermedio; no esperes que otro nodo lo vea.
Tarea 3: Confirmar qué contenedores usan el volumen
cr0x@server:~$ docker ps --format '{{.Names}} {{.Mounts}}' | grep pgdata
db pgdata
Qué significa: solo db usa este volumen.
Decisión: puedes programar tiempo de inactividad solo para ese servicio; no hay consumidores ocultos.
Tarea 4: Medir tamaño del volumen desde un contenedor utilitario (confía pero verifica)
cr0x@server:~$ docker run --rm -v pgdata:/v alpine:3.20 sh -lc 'du -sh /v && df -h /v | tail -n +2'
12.4G /v
/dev/sda2 220G 96G 113G 46% /v
Qué significa: unos 12.4G de datos, el sistema de ficheros destino tiene espacio.
Decisión: puedes usar un stream tar único; no hace falta fragmentación, pero planea espacio temporal si generas archivos de archivo.
Tarea 5: Comprobar tipo de sistema de ficheros y opciones de montaje (rendimiento y corrección)
cr0x@server:~$ findmnt -no SOURCE,FSTYPE,OPTIONS /var/lib/docker
/dev/sda2 ext4 rw,relatime
Qué significa: ext4, opciones estándar.
Decisión: rsync/tar debería comportarse de forma predecible. Si ves NFS/CIFS aquí, reducirías la velocidad y validarías bloqueo y comportamiento de fsync.
Tarea 6: Detener el escritor limpiamente (o estás copiando un objetivo en movimiento)
cr0x@server:~$ docker stop -t 60 db
db
Qué significa: el contenedor se detuvo dentro del tiempo de espera.
Decisión: procede con la sincronización final de corte. Si no se detiene, necesitas diagnosticar los hooks de apagado antes de migrar nada.
Tarea 7: Copia previa entre dos volúmenes en el mismo host (rsync)
cr0x@server:~$ docker volume create pgdata_new
pgdata_new
cr0x@server:~$ docker run --rm -i \
-v pgdata:/from:ro \
-v pgdata_new:/to \
alpine:3.20 sh -lc 'apk add --no-cache rsync >/dev/null && rsync -aHAX --numeric-ids --info=stats2 /from/ /to/'
Number of files: 14832 (reg: 12110, dir: 2711, sym: 11)
Number of created files: 14832 (reg: 12110, dir: 2711, sym: 11)
Total file size: 13,274,991,224 bytes
Total transferred file size: 13,274,991,224 bytes
Literal data: 13,274,991,224 bytes
Matched data: 0 bytes
File list size: 0
File list generation time: 0.210 seconds
File list transfer time: 0.000 seconds
Total bytes sent: 13,278,112,901
Total bytes received: 412,220
sent 13,278,112,901 bytes received 412,220 bytes 34,201,351.55 bytes/sec
total size is 13,274,991,224 speedup is 1.00
Qué significa: rsync preservó atributos (-aHAX), usó IDs numéricos, creó el número esperado de archivos.
Decisión: seguro para proceder a la verificación. Si “created files” es sorprendentemente bajo, probablemente apuntaste a la ruta de origen o volumen equivocado.
Tarea 8: Verificar que el conteo de archivos coincide (sanidad económica)
cr0x@server:~$ docker run --rm -v pgdata:/a -v pgdata_new:/b alpine:3.20 sh -lc 'cd /a && find . | wc -l; cd /b && find . | wc -l'
14865
14865
Qué significa: los conteos coinciden.
Decisión: procede. Si no coinciden, detente. Averigua qué se omitió (permisos, archivos especiales, problemas de montaje).
Tarea 9: Comprobar checksums por muestreo representativo (capturar corrupción silenciosa)
cr0x@server:~$ docker run --rm -v pgdata:/a -v pgdata_new:/b alpine:3.20 sh -lc '
apk add --no-cache coreutils >/dev/null
cd /a
find . -type f | head -n 200 | while read f; do
sha256sum "$f"
done | sort -k2 > /tmp/a.sha
cd /b
find . -type f | head -n 200 | while read f; do
sha256sum "$f"
done | sort -k2 > /tmp/b.sha
diff -u /tmp/a.sha /tmp/b.sha | head
'
Qué significa: sin salida de diff significa que las muestras coinciden.
Decisión: acepta la migración con mayor confianza. Si ves discrepancias, sospecha errores de disco subyacentes, RAM defectuosa o una herramienta de copia que no preservó contenido.
Tarea 10: Cortar a un servicio Compose al nuevo volumen
cr0x@server:~$ docker compose up -d
[+] Running 1/1
✔ Container db Started
Qué significa: el servicio está arriba.
Decisión: no celebres aún; valida la salud a nivel de aplicación y revisa los logs. “Started” no es “correcto”.
Tarea 11: Validar logs por pistas de permisos y corrupción inmediatamente tras el corte
cr0x@server:~$ docker logs --tail=80 db
PostgreSQL Database directory appears to contain a database; Skipping initialization
LOG: database system was shut down at 2026-02-04 09:12:30 UTC
LOG: database system is ready to accept connections
Qué significa: arranque limpio, reconoce el directorio de datos existente.
Decisión: procede a una prueba de humo (smoke test) a nivel de aplicación. Si ves “permission denied” o “invalid checkpoint record”, detente y revierte al volumen antiguo.
Tarea 12: Confirmar que el contenedor ve el uso esperado del disco (evitar sorpresas de “volumen vacío”)
cr0x@server:~$ docker exec db sh -lc 'du -sh /var/lib/postgresql/data | cat'
12.4G /var/lib/postgresql/data
Qué significa: el nuevo volumen está montado y poblado.
Decisión: sigue monitoreando, luego retira el volumen antiguo solo tras una ventana segura.
Tarea 13: Migrar un volumen entre hosts con un stream tar sobre SSH (sin archivo de staging)
cr0x@server:~$ docker run --rm -v pgdata:/from alpine:3.20 sh -lc 'cd /from && tar -cpf - . ' | ssh ops@newhost 'docker volume create pgdata && docker run --rm -v pgdata:/to alpine:3.20 sh -lc "cd /to && tar -xpf -"'
pgdata
Qué significa: datos transmitidos directamente; sin archivo intermedio.
Decisión: usa esto cuando necesites una transferencia directa y con pocas dependencias. Si necesitas reanudar/reintentar parcialmente, prefiere rsync.
Tarea 14: Comprobar Docker data-root si vas a reubicar todo el almacenamiento del motor
cr0x@server:~$ docker info --format '{{.DockerRootDir}}'
/var/lib/docker
Qué significa: ubicación del data-root del motor.
Decisión: si tu problema real es “el disco está lleno”, puede que necesites mover data-root o hacer prune—no confundas eso con la migración de un solo volumen.
Tarea 15: Diagnosticar el driver de almacenamiento y comprobar que no mezclas preocupaciones
cr0x@server:~$ docker info --format 'Driver={{.Driver}}'
Driver=overlay2
Qué significa: overlay2 es el driver de almacenamiento.
Decisión: guarda tu estado en volúmenes; no intentes “migrar una base de datos” copiando directorios de capas overlay2.
Tarea 16: Detectar SELinux en modo de aplicación que pueda romper el acceso tras la migración
cr0x@server:~$ getenforce
Enforcing
Qué significa: SELinux está en modo enforcing.
Decisión: si migras a un bind mount, probablemente necesitarás etiquetas adecuadas (p. ej., :Z/:z) o relabels. Los volúmenes nombrados suelen evitar este problema, pero las diferencias entre hosts siguen importando.
Patrones de migración seguros (mismo host, nuevo disco, nuevo host)
Patrón A: mismo host, migración de volumen a volumen (el aburrido ganador)
Si puedes mantener el motor Docker en el mismo host y solo necesitas mover datos a un almacen subyacente distinto, crea un volumen nuevo y copia en él. Esto mantiene la metadata del motor estable.
El método de copia puede ser rsync (mejor para sincronizaciones incrementales) o tar (mejor por simplicidad).
Usa rsync si:
- Quieres una copia previa + copia final para el cutover.
- Esperas reintentos o progreso parcial.
- Quieres estadísticas y comportamiento de diff sencillo.
Usa tar si:
- Quieres dependencias mínimas (tar está en todas partes).
- Estás transmitiendo por SSH y no quieres archivos temporales.
- Puedes permitir una pasada todo-o-nada.
Un detalle que la gente omite: ejecuta tu herramienta de copia dentro de un contenedor para que vea los montajes exactamente como los ven los contenedores. Eso evita la confusión de rutas del host y reduce la deriva de “funciona en mi host”.
Patrón B: migrar a un bind mount (cuando quieres control explícito)
Esto es lo que haces cuando quieres tus datos en /srv/data/postgres porque tu sistema de backups, monitorización y auditores ya entienden esa ruta.
Puede ser correcto. También puede ser un festival de permisos.
La ruta más segura es:
- Crea el directorio destino.
- Establece la propiedad y permisos para que coincidan con el UID/GID esperado por el contenedor.
- Copiar datos con rsync preservando IDs numéricos.
- Móntalo con opciones conscientes de SELinux si aplica.
Aquí es donde Docker sin root complica la vida: el UID dentro del contenedor puede no mapear como crees en el host. No hagas “chmod 777” en producción a menos que tu modelo de amenaza sea “ninguno”.
Patrón C: migrar entre hosts (tar stream o rsync sobre SSH)
La migración entre hosts es donde eliges entre sencillez y reanudabilidad.
- tar sobre SSH es sencillo y rápido de configurar, con pocas piezas móviles.
- rsync sobre SSH es tu amigo cuando la transferencia es grande, la red es inestable o quieres warm-sync y luego final-sync.
Para rsync entre hosts sin exponer la ruta del volumen del host, aún puedes hacerlo contenedor-a-contenedor montando el volumen en un contenedor utilitario y ejecutando rsync hacia fuera por SSH. Es algo más de tecleo, mucha menos confusión.
Patrón D: mover todo el data-root de Docker (último recurso, hazlo bien)
A veces el requisito real es “mover el almacenamiento de Docker fuera del disco raíz”. Eso no es una migración de volúmenes; es reubicar el data-root del motor. La secuencia correcta es:
detener Docker, copiar data-root preservando, actualizar la configuración del daemon, iniciar Docker y luego validar imágenes/contenedores/volúmenes.
No hagas esto para “arreglar” un único volumen de aplicación a menos que te guste crear radios de explosión más grandes de los necesarios.
Verificación que realmente detecta migraciones malas
La gente de almacenamiento cobra por desconfiar de las señales de éxito. Que un contenedor esté “Up” significa que el proceso init no se estrelló todavía. No significa que tus datos sean correctos.
La verificación necesita capas:
Capa 1: sanidad a nivel de sistema de ficheros
- Chequeo de tamaño: orden de magnitud esperado, no bytes exactos.
- Conteo de archivos: detecta rápidamente errores de “directorio copiado vacío”.
- Chequeo de permisos/propiedad: detecta desajustes de UID/GID y xattrs faltantes.
Capa 2: muestreo a nivel de contenido
Checksums completos en volúmenes multi-terabyte pueden ser irrazonables durante una ventana de mantenimiento. Muestrea inteligentemente:
- Hashea los archivos modificados más recientemente.
- Hashea directorios conocidos y críticos (WAL, índices, metadatos).
- Hashea muestras aleatorias a lo largo del árbol.
Capa 3: verdad a nivel de aplicación
Para bases de datos, ejecuta una consulta de lectura y una de escritura. Para almacenes de objetos, recupera y vuelve a subir un objeto. Para colas, encola y desencola. Valida aquello que realmente le importa al negocio.
Disciplina de rollback
Mantén el volumen antiguo intacto y desconectado. No “limpies” hasta que hayas superado el punto donde surgirían errores silenciosos. Si el ciclo de negocio es un informe semanal, “mañana” no es suficiente.
Segundo chiste (y el último): Las copias de seguridad son como paracaídas—si las necesitas y no las tienes, no las volverás a necesitar.
Tres mini-historias corporativas desde las trincheras de almacenamiento
Mini-historia 1: El incidente causado por una suposición equivocada
Una empresa mediana ejecutaba una API orientada al cliente en un único host Linux con Docker Compose. La base de datos estaba en un volumen nombrado.
Un ingeniero de plataforma decidió “simplificar las copias de seguridad” copiando /var/lib/docker/volumes cada noche a un NAS usando una herramienta genérica de sincronización de archivos.
La suposición: un volumen es un directorio, y copiar directorios es lo mismo que hacer backup. Funcionó durante semanas. El trabajo de backup reportaba éxito. El NAS se llenó poco a poco. Todos siguieron con sus tareas.
Entonces un problema de disco forzó una restauración. Copiaron el directorio de vuelta, reiniciaron el contenedor de la base de datos y la BD arrancó con un subconjunto de tablas faltantes de escrituras recientes.
La causa raíz fue dolorosamente normal. El backup se ejecutaba mientras la base de datos escribía. La herramienta de copia produjo una vista puntual que nunca existió: algunos archivos de “antes” y algunos de “después”, más algunos segmentos parcialmente copiados.
No hubo snapshot a nivel de BD, ni silenciamiento, ni prueba de restauración de verificación. El “backup” era un montón de archivos con nombres plausibles.
Arreglarlo requirió reconstruir desde exportaciones lógicas que, por suerte, aún estaban disponibles para parte de los datos. La lección dura no fue “no uses Docker”.
Fue: no confundas “copia de archivos exitosa” con “datos consistentes”.
Mini-historia 2: La optimización que salió mal
Otra organización ejecutaba cargas de CI que producían muchos artefactos. Migraron cachés de compilación a un sistema de ficheros de red compartido y lo montaron en contenedores para reducir el desgaste de SSD locales.
La propuesta era fantástica: menos upgrades de SSD locales, gestión centralizada, limpieza más sencilla.
El rendimiento se veía bien el primer día. Luego aparecieron algunos bugs sutiles: compilaciones que fallaban ocasionalmente de formas extrañas, tests que agotaban el tiempo y un aumento de errores “file not found” que desaparecían al reintentar.
Los ingenieros culparon a tests intermitentes. Los SRE culparon a la red. Todos tenían un punto válido.
El fallo vino de la semántica del sistema de ficheros. El share de red tenía caching y comportamientos de bloqueo que no coincidían con las suposiciones de disco local.
Algunas herramientas esperaban renames atómicos y fsync fiable; el share a veces retrasaba o reordenaba la visibilidad bajo carga. No estaba “caído”. Estaba “diferente”.
La solución fue mantener artefactos verdaderamente compartidos en object storage (con semánticas de publicación explícitas), y mantener scratch por trabajo en disco local.
La “optimización” no falló porque NFS sea malvado; falló porque la carga era sensible a latencia y asumía garantías del sistema de ficheros local.
Mini-historia 3: La práctica aburrida que salvó el día
Un equipo de servicios financieros tenía una regla: cualquier contenedor con estado debía tener un plan de corte escrito, un plan de reversión y un paso de verificación que incluya una transacción real.
A nadie le encantaba el papeleo. Pero hacía las migraciones repetibles.
Necesitaron mover un volumen Postgres a un host nuevo por un cambio de contrato de mantenimiento. El plan de migración usó un rsync previo mientras la BD estaba en ejecución, luego una parada programada, luego un rsync final y comprobaciones a nivel de aplicación.
El plan también incluía “mantener el volumen antiguo en solo lectura y desconectado durante siete días”.
En la noche del corte todo pareció correcto. Logs limpios. Checks de salud en verde.
A la mañana siguiente un job de reporting encontró una inconsistencia en una tabla derivada. No catastrófica, pero sospechosa.
Como mantuvieron el volumen antiguo, pudieron montarlo en solo lectura y comparar un pequeño conjunto de archivos y marcas de tiempo.
Encontraron el problema: un cambio de esquema de última hora había ocurrido durante la ventana de rsync previo, y el proceso de corte omitió un archivo accesorio creado por un sidecar fuera del contenedor de la BD.
Reejecutaron una sincronización dirigida de ese directorio y revalidaron. Sin drama, sin outage prolongado, sin “borramos los viejos datos porque estábamos seguros”.
Ganó lo aburrido. De nuevo.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: el contenedor nuevo arranca “como nuevo” como si faltaran datos
Causa raíz: montaste un volumen vacío (nombre nuevo) o cambiaste un volumen nombrado por una ruta bind que apunta a un directorio vacío.
Solución: inspecciona montajes con docker inspect. Confirma el nombre del volumen en Compose. Poblá el destino correcto y reinicia.
2) Síntoma: “permission denied” tras la migración
Causa raíz: desajuste UID/GID, mapeos de Docker sin root, o mismatch de etiquetas SELinux en bind mounts.
Solución: usa --numeric-ids con rsync; ajusta la propiedad para el usuario de ejecución del contenedor; aplica etiquetado SELinux correcto para bind mounts.
3) Síntoma: la base de datos arranca y luego se cae con errores tipo corrupción
Causa raíz: copiaste mientras la BD escribía, produciendo un estado en disco inconsistente.
Solución: detén/silencia la BD para la sincronización final, o usa herramientas nativas de backup/snapshot de la BD. No confíes en una copia de archivos en crudo de un datastore vivo.
4) Síntoma: la migración es extremadamente lenta, la CPU al máximo
Causa raíz: sobrecarga de cifrado SSH o compresión mal configurada, especialmente en instancias con pocas CPU.
Solución: mide la CPU en ambos extremos; considera desactivar compresión; considera usar un cifrado más rápido o mover la copia a una red privada. O realiza staging local y transfiere por un canal mejor.
5) Síntoma: rsync termina, pero el conteo de archivos difiere
Causa raíz: patrones excluidos, errores de permisos, archivos especiales omitidos, o copiaste desde la ruta de montaje equivocada.
Solución: vuelve a ejecutar rsync con verbose e itemized changes, revisa stderr, ejecuta como root en el contenedor utilitario si es necesario y valida montajes fuente/objetivo.
6) Síntoma: tras el corte, la app está “sana” pero el rendimiento colapsa
Causa raíz: el almacenamiento destino tiene distintas características de latencia (HDD vs SSD, almacenamiento en red vs local, opciones de montaje diferentes).
Solución: ejecuta un pequeño benchmark de lectura/escritura antes del corte; revisa el sistema de ficheros y opciones de montaje; para bases de datos, confirma fsync y comportamiento de barreras compatible con los requisitos.
7) Síntoma: los datos parecen presentes, pero marcas de tiempo/propiedad cambiaron inesperadamente
Causa raíz: las opciones por defecto de la herramienta de copia no preservaron metadata (p. ej., falta -a), o usaste tar sin preservar permisos.
Solución: usa rsync con -aHAX cuando corresponda; usa tar con -p y verifica el comportamiento de extracción; valida con muestreos de stat.
8) Síntoma: el contenedor no puede montar el volumen después de migrar el host
Causa raíz: mismatch de driver de volumen o plugin faltante en el host destino (común con drivers de terceros).
Solución: confirma driver y opciones del volumen; instala el mismo plugin/driver; si migras de local a un volumen gestionado por plugin, trátalo como una decisión de arquitectura nueva, no como una simple copia de archivos.
Listas de verificación / plan paso a paso
Checklist A: Pre-vuelo (antes de tocar los datos)
- Inventario de montajes para el servicio: volúmenes nombrados, bind mounts, tmpfs.
- Clasificar la carga: base de datos/cola/objeto vs “archivos estáticos”. Si es transaccional, planifica silencio o backup nativo.
- Medir tamaño del volumen y confirmar espacio libre del destino.
- Decidir método de copia: rsync para incremental, tar para simplicidad/transmisión.
- Decidir ventana de downtime y límite de tiempo para rollback.
- Escribir pasos de verificación que incluyan una comprobación a nivel de aplicación.
Checklist B: Sincronización previa (opcional pero recomendado)
- Crea el volumen destino (o el directorio destino para bind mount).
- Ejecuta rsync del antiguo al nuevo mientras la app sigue en ejecución.
- Registra conteos de archivos y tamaños aproximados.
- No borres nada todavía.
Checklist C: Sincronización de corte (la parte que previene pérdida de datos)
- Detén la aplicación limpiamente (o pausa escrituras vía mecanismo de la app).
- Ejecuta un rsync final (o copia tar) para capturar los últimos cambios.
- Verifica conteos de archivos y checksums muestreados.
- Actualiza la definición de Compose/servicio para apuntar al nuevo volumen/bind mount.
- Inicia el servicio y observa logs los primeros minutos.
- Ejecuta una prueba de humo a nivel de aplicación (lectura y escritura).
Checklist D: Plan de rollback (escríbelo antes de empezar)
- Si la verificación falla, detén el servicio.
- Vuelve a apuntar al volumen antiguo.
- Inicia el servicio, confirma salud.
- Conserva el volumen migrado fallido para forense; no “arregles” sobrescribiendo la evidencia.
Checklist E: Higiene post-corte (no lo hagas demasiado pronto)
- Monitorea durante un ciclo de negocio completo (jobs batch, informes, backups).
- Sólo entonces archiva o elimina el volumen antiguo.
- Actualiza los runbooks para que la próxima migración no sea una hazaña puntual.
FAQ
1) ¿Cuál es la forma más segura de migrar un volumen nombrado Docker?
Detén el escritor para la sincronización final, luego copia del antiguo al nuevo usando un contenedor utilitario que monte ambos volúmenes.
Prefiere rsync para migraciones en dos pasadas; verifica con conteos de archivos y una comprobación a nivel de aplicación.
2) ¿Puedo simplemente copiar /var/lib/docker/volumes?
Puedes, pero normalmente no deberías. Es fácil copiar lo equivocado, y copiar datos en vivo es inconsistente para bases de datos.
Si debes hacerlo, detén Docker completamente y trátalo como una reubicación del data-root de Docker, no como “una copia de volumen”.
3) ¿Es tar mejor que rsync?
Tar es más simple y genial para streaming. Rsync es mejor para copias incrementales, reintentos y un enfoque warm+final.
Para evitar pérdida de datos, la clave no es tar vs rsync; es silenciar escrituras y verificar resultados.
4) ¿Cómo migrar volúmenes entre hosts sin conocer las rutas internas de Docker?
Usa un contenedor utilitario con el volumen montado y transmite tar sobre SSH a otro contenedor utilitario que extraiga en un volumen destino.
Eso te mantiene fuera de los internos de Docker.
5) ¿Y las bases de datos en contenedores—debo copiar el directorio de datos?
Solo si puedes garantizar un estado en disco consistente (servicio detenido, o snapshot del sistema de ficheros con garantías correctas).
Backups/replicación nativa de la BD suele ser más seguro para cero/bajo downtime, pero es un proyecto mayor que una “copia”.
6) ¿Por qué mi contenedor perdió permisos tras mover a un bind mount?
Porque los bind mounts exponen la propiedad/etiquetas del sistema de ficheros del host directamente. Los volúmenes nombrados tienden a ser más simples.
Soluciona igualando UID/GID, usando preservación de IDs numéricos durante la copia y gestionando etiquetas SELinux si está en modo enforcing.
7) ¿Puedo renombrar un volumen Docker?
No directamente. La “renombrada” práctica es: crea un volumen nuevo con el nombre deseado, copia los datos dentro, actualiza referencias y elimina el volumen antiguo más tarde.
8) ¿Cómo evito downtime por completo?
Para sistemas con estado, “sin downtime” suele significar replicación: configura una instancia nueva, replica/stream cambios y luego cambia el tráfico.
Una migración de volumen en crudo suele implicar un breve downtime salvo que la carga sea de solo lectura.
9) ¿Cuál es la mayor señal de alarma durante la migración?
Cuando el servicio nuevo arranca “limpio” e inicializa un directorio de datos fresco. Eso normalmente significa que no vio los datos migrados.
Detente inmediatamente y confirma montajes.
10) ¿Cuándo está bien borrar el volumen antiguo?
Tras la verificación y después de haber pasado un ciclo de negocio completo que pueda sacar a la luz problemas sutiles.
Consérvalo más tiempo si compliance o forense lo requieren; el almacenamiento es más barato que la respuesta a un incidente.
Próximos pasos que puedes hacer esta semana
Si ejecutas contenedores con estado en producción, realiza estas acciones prácticas:
- Escribe un runbook de migración usando el enfoque de contenedor utilitario (montar antiguo + nuevo, rsync/tar, verificar, corte, rollback). Hazlo el predeterminado.
- Añade verificación a tu definition of done: conteo de archivos + checksums muestreados + comprobación de lectura/escritura a nivel de aplicación.
- Etiqueta tus volúmenes (en Compose o mediante convenciones de nombres) para saber cuáles son stateful y cuáles caches descartables.
- Programa una prueba de restauración desde lo que llames “backup”. La forma más rápida de descubrir que es falsa es un martes por la tarde, no durante un outage.
- Decide tu postura sobre bind mounts vs volúmenes nombrados y documenta la decisión. Titubear es cómo terminas con tres patrones de persistencia y sin una historia consistente de backups.
El “truco de migración de volúmenes” no es magia. Es disciplina: monta los datos como los monta la app, copia preservando metadata, detén el escritor para la pasada final y verifica como si no confiaras en ti mismo. No deberías confiar.