Migración de stack Docker Compose: mover a un nuevo host sin mitos sobre el tiempo de inactividad

¿Te fue útil?

Tienes un stack Docker Compose que ha estado “bien durante años” y ahora necesitas moverlo a un nuevo host porque el antiguo está fallando, fuera de garantía, sin espacio o fuera de la paciencia de todos.
El negocio pide “sin tiempo de inactividad” y alguien dice: “Simplemente rsyncea los volúmenes y arráncalo allí”. Esa frase es donde nacen los incidentes.

Compose se puede migrar limpiamente. Pero si quieres que los usuarios experimenten “sin tiempo de inactividad”, debes definir qué significa eso (¿minutos? ¿segundos? ¿algunos errores?) y elegir una técnica de migración que coincida con tus patrones de datos y tráfico.
Tus contenedores web son fáciles. Tus bases de datos son la parte que te manda a terapia.

El mito de “sin tiempo de inactividad”: qué puedes y qué no puedes prometer

“Sin tiempo de inactividad” no es binario. Es un contrato. Necesitas decidir qué estipula el contrato y luego construir una migración que lo cumpla.
Para migraciones con Docker Compose, el factor limitante casi siempre es la data con estado: bases de datos, colas y cualquier volumen escribible que importe.
Si tu stack es completamente sin estado, puedes hacer un intercambio blue/green limpio con impacto casi nulo para el usuario.
Si tu stack escribe en disco, debes:

  • Replicar el estado (replicación de bases de datos, replicación de almacenamiento de objetos, escritura dual), y luego conmutar, o
  • Congelar las escrituras (ventana de mantenimiento, modo solo lectura) y copiar datos consistentes, o
  • Aceptar tiempo de inactividad y hacerlo corto y predecible.

La respuesta honesta de un SRE es: si estás copiando volúmenes que están siendo escritos, no tienes “sin tiempo de inactividad”. Tienes “corrupción eventual con un lado de negación”.
La versión aceptable de “sin tiempo de inactividad” suele ser “sin ventana de mantenimiento planificada”, lo que aún puede permitir unos segundos de picos durante cambios de DNS o balanceador.

Aquí está la definición práctica que me gusta: sin tiempo de inactividad para usuarios, pero con un pequeño presupuesto de errores permitido.
Por ejemplo: un pico de 30 segundos de 502 en la conmutación es tolerable si avisas a la gente y tus clientes reintentan.
Una inconsistencia silenciosa de datos de dos horas porque rsynceaste un volumen de base de datos en vivo no lo es.

Broma corta nº 1: “Lo haremos con cero downtime” suele ser abreviatura de “aún no hemos mirado la base de datos”.

Algunos hechos e historia (para que dejes de creer en magia)

No necesitas convertirte en un arqueólogo de contenedores para migrar un stack Compose. Pero un poco de contexto te ayuda a predecir modos de fallo.
Aquí hay hechos concretos que importan en migraciones reales:

  1. Los volúmenes de Docker fueron diseñados para persistencia local, no para portabilidad. Los volúmenes nombrados viven bajo Docker’s data root y no son inherentemente “movibles” sin copiar el sistema de archivos subyacente.
  2. Docker Compose empezó como una herramienta externa en Python. Más tarde se convirtió en un plugin del CLI de Docker (Compose V2), lo que cambió comportamientos y formatos de salida lo suficiente como para confundir runbooks.
  3. La red overlay es de Swarm/Kubernetes, no de Compose. La red de Compose es bridge de un solo host a menos que traigas tu propia tela de red.
  4. Las direcciones IP dentro de redes Docker no son contratos estables. Si una aplicación depende de IPs de contenedores, ya es un incidente menor esperando una invitación de calendario.
  5. Rsync de un directorio de base de datos en vivo no es una copia de seguridad. La mayoría de bases de datos necesitan snapshots, congelación del sistema de archivos o herramientas nativas de backup para obtener una copia consistente.
  6. Los healthchecks existen porque “contenedor iniciado” no es lo mismo que “servicio listo”. Úsalos en cutovers o disfruta de misteriosos 502.
  7. Los TTL de DNS son orientativos, no absolutos. Algunos resolutores cachean más tiempo del que pediste. Diseña cutovers asumiendo rezagados.
  8. Compose no es un scheduler. No reprogramará tu contenedor fallido en otro host. Si necesitas eso, estás en territorio de Swarm/Kubernetes (o estás construyendo tu propia automatización, lo cual es… un hobby).
  9. La propiedad de archivos y el mapeo UID/GID son asesinos silenciosos. Cambios de imagen y diferencias en el host rompen permisos en bind mounts y volúmenes de maneras que parecen “bugs de la app”.

Una cita, porque sigue siendo el trabajo: La esperanza no es una estrategia.

Modelos de migración que realmente funcionan

Modelo A: blue/green sin estado (el caso soñado)

Si tu stack no tiene escrituras persistentes en el host (o todas las escrituras van a servicios gestionados), haz esto:
levanta el mismo proyecto Compose en el nuevo host, verifica los healthchecks y luego cambia el tráfico vía DNS, proxy inverso o balanceador.
Mantén el host antiguo en funcionamiento como fallback hasta que confíes en el nuevo.

El impacto visible al usuario puede ser casi nulo si:
las sesiones no están atadas a un host, o usas almacenamiento de sesiones compartido (Redis, sesiones en BD, JWTs),
y tu método de conmutación no deja la mitad del tráfico en endpoints obsoletos.

Modelo B: con estado y replicación (el enfoque maduro)

Para PostgreSQL/MySQL/Redis, la replicación es la manera más limpia de abordar “sin tiempo de inactividad”.
Ejecutas la nueva base de datos como réplica, la dejas ponerse al día y luego la promueves.
Esto funciona bien si puedes tolerar una breve congelación de escrituras en el momento de la promoción o si tu aplicación soporta failover rápido.

Las migraciones basadas en replicación trasladan el riesgo de “correctitud de copia de datos” a “correctitud de replicación y coreografía del cutover”.
Es un buen intercambio: las herramientas de replicación fueron hechas para esto; tu script de rsync fue hecho para dar confianza.

Modelo C: snapshot + breve congelación (la realidad intermedia)

Si la replicación es demasiado pesada (apps legadas, sin tiempo, sin experiencia), apunta a un snapshot consistente:
detén las escrituras, toma un snapshot (LVM/ZFS/btrfs o backup nativo de la BD), transfiérelo, arranca en el nuevo host y conmuta.
El tiempo de inactividad es el intervalo de “detener escrituras” más el tiempo de conmutación.

Modelo D: “Solo copiar /var/lib/docker” (la trampa)

Esto puede funcionar en condiciones muy concretas: misma versión de Docker, mismo driver de almacenamiento, mismas semánticas del sistema de archivos, sin escrituras en vivo y estás preparado para depurar internals de Docker a las 3 a.m.
Si estás migrando porque el host viejo es frágil, duplicar técnicas frágiles es una elección estética.

Prevuelo: qué inventariar antes de tocar nada

Los fallos en migraciones rara vez vienen de lo obvio. Vienen del acoplamiento invisible entre tu archivo Compose y tu host:
rutas de sistema de archivos, ajustes del kernel, reglas de firewall y cron jobs “temporales” que se volvieron producción.

Antes de construir el nuevo host, inventaría esto:

  • Versión de Compose y versión del Docker Engine en el host antiguo (coincidir o actualizar intencionalmente).
  • Volúmenes: volúmenes nombrados vs bind mounts; qué servicios escriben; cuánto dato.
  • Secretos/config: archivos .env, directorios de configuración montados, cert TLS, claves API.
  • Ingreso: proxy inverso, puertos publicados, firewall, reglas NAT.
  • Dependencias externas: registros DNS, listas de permitidos, webhooks upstream, relés SMTP.
  • Observabilidad: ubicación de logs, endpoints de métricas, integraciones de alertas.
  • Estrategia de backup/restore que puedas probar sin apostar la paciencia de la empresa.

Tareas prácticas con comandos (y cómo interpretarlos)

Esta sección es deliberadamente práctica: comandos que puedes ejecutar hoy, salidas que deberías esperar y la decisión que tomas a partir de cada uno.
Úsalos primero en el host antiguo y luego repítelos en el nuevo host como validación.

Task 1: Identify Docker and Compose versions (compat risk)

cr0x@server:~$ docker version --format 'Engine={{.Server.Version}} StorageDriver={{.Server.Driver}}'
Engine=26.1.4 StorageDriver=overlay2

Significado: Tienes Docker Engine 26.x usando overlay2. Eso es bueno y común.
Decisión: En el nuevo host, iguala la versión mayor (o prueba la actualización). También asegura que overlay2 esté soportado (kernel + sistema de archivos).

cr0x@server:~$ docker compose version
Docker Compose version v2.27.1

Significado: Plugin Compose V2, no el docker-compose V1 heredado.
Decisión: Usa la misma versión mayor/menor si quieres menos sorpresas en nombres de red, nombres de proyecto y parsing de salida del CLI.

Task 2: See what’s actually running (and under what project name)

cr0x@server:~$ docker compose ls
NAME            STATUS              CONFIG FILES
payments        running(7)          /srv/payments/compose.yaml

Significado: Tu proyecto Compose se llama payments, no el nombre de carpeta que imaginaste.
Decisión: Mantén el nombre de proyecto estable entre hosts (usa --project-name o name: en Compose) para evitar sorpresas en nombres de redes/volúmenes.

Task 3: Enumerate services, images, and ports (ingress impact)

cr0x@server:~$ docker compose -p payments ps
NAME                 IMAGE                        COMMAND                  SERVICE   STATUS    PORTS
payments-web-1       nginx:1.27-alpine            "/docker-entrypoint.…"   web       running   0.0.0.0:443->443/tcp
payments-api-1       ghcr.io/acme/api:3.14.2      "/app/start"             api       running   0.0.0.0:8080->8080/tcp
payments-db-1        postgres:15                  "docker-entrypoint.s…"   db        running   5432/tcp

Significado: Solo web y api publican puertos al host. La base de datos es interna solamente (bien).
Decisión: Tu superficie de conmutación son 443 y 8080, no 5432. Eso cambia la planificación de firewall y balanceador.

Task 4: List volumes and identify what’s stateful

cr0x@server:~$ docker volume ls
DRIVER    VOLUME NAME
local     payments_dbdata
local     payments_redisdata
local     payments_prometheus

Significado: Existen volúmenes nombrados. Espéralos bajo el Docker data root a menos que uses driver_opts.
Decisión: Trata dbdata y redisdata como stateful y requierelos garantías de consistencia.

Task 5: Determine where Docker stores data (so you don’t copy the wrong disk)

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

Significado: Ruta raíz por defecto de Docker.
Decisión: Si planeas snapshots del sistema de archivos, esta ruta debe estar en un filesystem con capacidad de snapshot y debes conocer su dispositivo/pool subyacente.

Task 6: Inspect mounts used by a container (named volume vs bind mount truth)

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

Significado: La base de datos escribe en un volumen nombrado bajo /var/lib/docker/volumes.
Decisión: Si lo copias, necesitas un método consistente (snapshot o backup nativo de la BD), no una copia casual de archivos mientras está en ejecución.

Task 7: Check volume size and growth (transfer time and risk)

cr0x@server:~$ sudo du -sh /var/lib/docker/volumes/payments_dbdata/_data
58G	/var/lib/docker/volumes/payments_dbdata/_data

Significado: Estás moviendo ~58 GB de datos de la base de datos, además de WAL y overhead.
Decisión: Planifica el tiempo de transferencia según el throughput real. Si no puedes replicar y no puedes permitirte una copia larga, necesitas otro modelo.

Task 8: Find hidden bind mounts referenced in Compose

cr0x@server:~$ grep -nE '^\s*-\s*/' /srv/payments/compose.yaml
42:      - /srv/payments/nginx/conf.d:/etc/nginx/conf.d:ro
43:      - /srv/payments/tls:/etc/nginx/tls:ro
77:      - /srv/payments/uploads:/var/lib/app/uploads

Significado: Usas bind mounts para config, TLS y uploads. Uploads son stateful y no están dentro de volúmenes Docker.
Decisión: Añade esos directorios al plan de migración, incluyendo propiedad y permisos.

Task 9: Validate image availability (prevent cutover-day “pull denied”)

cr0x@server:~$ docker compose -p payments config --images
ghcr.io/acme/api:3.14.2
nginx:1.27-alpine
postgres:15
redis:7-alpine

Significado: Estas etiquetas de imagen exactas son las que ejecutarás.
Decisión: En el nuevo host, pre-pulla las imágenes. Si usas un registro privado, confirma que la autenticación funciona antes del cutover.

Task 10: Check container health and readiness gates (cutover correctness)

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}' | head
NAMES             STATUS
payments-web-1    Up 5 days
payments-api-1    Up 5 days (healthy)
payments-db-1     Up 5 days

Significado: El API reporta healthy. Web y DB pueden no tener healthchecks configurados (común).
Decisión: Añade healthchecks para cualquier servicio del que dependas en el cutover (especialmente proxy inverso y API). Los healthchecks no son decorativos.

Task 11: Confirm open ports and what’s bound (avoid stealth conflicts)

cr0x@server:~$ sudo ss -lntp | grep -E ':(443|8080)\s'
LISTEN 0      4096        0.0.0.0:443       0.0.0.0:*    users:(("docker-proxy",pid=15422,fd=4))
LISTEN 0      4096        0.0.0.0:8080      0.0.0.0:*    users:(("docker-proxy",pid=15457,fd=4))

Significado: Los puertos se publican vía docker-proxy. En algunos sistemas verás DNAT de iptables en su lugar, pero el resultado final es el mismo: esos puertos deben estar libres en el nuevo host.
Decisión: Reserva esos puertos. Si algo más (como un Nginx antiguo en el host) los está usando, fallarás al desplegar.

Task 12: Measure disk and inode headroom (migrations fail on boring math)

cr0x@server:~$ df -h /var/lib/docker
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p3  500G  410G   65G  87% /

Significado: Ya estás justo de espacio.
Decisión: El nuevo host debe tener holgura cómoda. Si migras a “mismo tamaño pero más nuevo”, solo compras tiempo, no solucionas el problema.

Task 13: Check filesystem type and mount options (snapshot and performance implications)

cr0x@server:~$ findmnt -no FSTYPE,OPTIONS /var/lib/docker
ext4	rw,relatime,errors=remount-ro

Significado: ext4, sin snapshots nativos.
Decisión: Si quieres snapshots consistentes del sistema de archivos sin detener servicios, necesitarás LVM debajo, o mover Docker root a ZFS/btrfs en el nuevo host, o usar backups nativos de BD.

Task 14: Validate DNS and TTL behavior you actually get

cr0x@server:~$ dig +noall +answer app.example.internal
app.example.internal.  300  IN  A  10.20.30.40

Significado: TTL es 300 segundos (5 minutos).
Decisión: Planifica hasta varios minutos de tráfico mixto a menos que uses load balancer/VIP para el cutover. Reduce TTL con antelación si DNS es tu interruptor.

Task 15: Test application-level write freeze readiness (if you need it)

cr0x@server:~$ curl -sS -o /dev/null -w '%{http_code}\n' https://app.example.internal/health
200

Significado: Endpoint de salud accesible.
Decisión: Si necesitas un modo mantenimiento, implántalo y pruébalo ahora. Una migración es un mal momento para descubrir que tu app no puede pasar a solo lectura limpiamente.

Almacenamiento y estado: la parte que no puedes ignorar

Las migraciones de Compose suelen enmarcarse como “mover contenedores”. Eso es lindo. Estás moviendo datos.
Los contenedores son ganado; tus volúmenes son mascotas con personería jurídica.

Volúmenes nombrados vs bind mounts: implicaciones de migración

Los volúmenes nombrados están dentro del plano de control de Docker. Eso es conveniente pero hace las migraciones opacas:
debes localizar la ruta de datos del volumen, copiarla de forma segura y preservar la propiedad.
Los bind mounts son explícitos: puedes ver la ruta en Compose, puedes respaldarla con herramientas estándar y aplicar prácticas de sistema de archivos.
La desventaja: los bind mounts están acoplados al host. Tu layout de directorios se vuelve un contrato de API.

Reglas de consistencia (una versión directa)

  • Si es una base de datos: usa replicación o herramientas nativas de backup; trata las copias a nivel de sistema de archivos como sospechosas a menos que tengas snapshots o la BD esté apagada limpiamente.
  • Si es un directorio de almacenamiento de objetos (uploads): rsync está bien, pero debes manejar escrituras concurrentes (sincronización en dos fases: copia inicial, luego sincronización final después de congelar escrituras).
  • Si es Redis: decide si es un caché (reconstruir) o un datastore (replicar o snapshot).
  • Si es Prometheus: puedes copiar, pero espera churn en WAL. Snapshotting funciona; rsync en vivo es inestable a menos que lo pares.

Dos patrones fiables para mover datos stateful

Patrón 1: Backup nativo de la base de datos + restore (predecible, usualmente más rápido)

Para PostgreSQL: usa pg_basebackup (replicación) o pg_dump/pg_restore (backup lógico).
Para MySQL: usa replicación o un volcado consistente con flags apropiados.
La ventaja clave: mueves datos en un formato que la base de datos entiende, no archivos que la base de datos está cambiando activamente.

Patrón 2: Snapshot + send (rápido para datasets grandes)

Si tu Docker root o directorios de volúmenes viven en ZFS o LVM, los snapshots te permiten tomar una copia puntal consistente a nivel de sistema de archivos.
Luego transfieres el snapshot al nuevo host y lo montas como origen del volumen.
Esto es extremadamente rápido para datasets grandes y reduce el downtime. También exige que planificaras tu layout de almacenamiento como un adulto.

Broma corta nº 2: El almacenamiento es el único lugar donde “funcionó en mi máquina” significa “tu máquina ahora es mi problema”.

Red y conmutación: DNS, VIPs y proxies inversos

La mayoría de stacks Compose tienen una de estas formas de ingreso:

  • Puertos publicados directamente (p. ej., 443 en el host) y los clientes se conectan a la IP/DNS del host.
  • Un contenedor proxy inverso (nginx/Traefik/Caddy) publicando 80/443, enroutando a servicios internos.
  • Un balanceador externo delante, reenviando a los host(s).

Para migraciones, la palanca de conmutación mejor es la que permite rollback más rápido:
un cambio en el pool del balanceador, mover un VIP, o una actualización de DNS que puedas revertir rápido.
Publicar puertos directamente en un único host con DNS hardcodeado es fácil hasta que llega el momento de mover.

Conmutación por DNS: bien, pero planifica los rezagados

El corte por DNS es común porque es accesible. También es desordenado:
caches ignoran TTLs, clientes reusan conexiones TCP y algún software “fija” la IP hasta reiniciarse.
Si haces DNS, baja los TTL al menos un día antes (no cinco minutos antes).
Luego mantén el servicio antiguo en ejecución hasta que estés seguro de haber drenado.

Conmutación VIP/keepalived: limpia si puedes ejecutarla

Un IP virtual movido entre hosts puede ser casi instantáneo y fácil de revertir.
Pero requiere soporte de red y la voluntad de ejecutar VRRP/keepalived correctamente.
En redes corporativas con control de cambios estricto, esto puede ser más difícil de lo que debería.

Conmutación por proxy inverso: pon el interruptor donde corresponde

Si ya tienes un proxy inverso, considera hacerlo externo a los hosts de la aplicación.
Una pequeña capa de proxies dedicada puede dirigir a backend antiguo o nuevo según la configuración.
Eso es efectivamente blue/green sin necesitar que Compose sea un scheduler.

Guion de diagnóstico rápido

Cuando una migración “funciona” pero el rendimiento cae o los errores se disparan, necesitas un camino corto hacia el cuello de botella.
No debatas arquitectura en el canal de incidentes. Revisa lo básico en orden.

Primero: ¿llega el tráfico al lugar correcto?

  • Verifica la resolución DNS desde múltiples redes (red corporativa, VPN y desde el nuevo host mismo).
  • Confirma que el nuevo host realmente está recibiendo conexiones en los puertos esperados (ss, contadores de firewall).
  • Revisa los upstreams del proxy inverso y el estado de salud.

Segundo: ¿la app está sana o solo “corriendo”?

  • Comprueba el estado de health de los contenedores, no solo el uptime.
  • Haz tail de logs en el borde (proxy) y en el núcleo (API) al mismo tiempo; correlaciona timestamps.
  • Busca agotamiento de pools de conexiones, timeouts y “permission denied” en mounts.

Tercero: ¿es el almacenamiento el cuello de botella?

  • Revisa latencia de disco y saturación (iostat, nvme smart stats, dmesg por resets).
  • Confirma que la base de datos está en el disco/pool esperado y no en un volumen de arranque lento.
  • Valida opciones del sistema de archivos y espacio libre; discos casi llenos se comportan mal.

Cuarto: ¿el camino de red es diferente al que crees?

  • Revisa desajustes de MTU (especialmente a través de VPNs y VLANs).
  • Verifica reglas de firewall y límites de conntrack en el nuevo host.
  • Mira retransmisiones SYN y resets TCP (estadísticas ss, logs del proxy).

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

1) La API devuelve 502/504 justo después del cutover

Síntoma: El proxy inverso está arriba, pero las llamadas a upstream time out.

Causa raíz: Faltaron healthchecks o eran demasiado débiles; el proxy empezó a enrutar antes de que la API se calentara o terminaran las migraciones.

Solución: Añade healthchecks reales (conectividad a BD, no solo proceso vivo). Gatea el ruteo del proxy en función de salud. Considera el orden de arranque con depends_on más condiciones de health.

2) La base de datos arranca y luego se cae con errores de corrupción

Síntoma: Postgres se queja de checkpoints inválidos o segmentos WAL; MySQL reporta errores de InnoDB.

Causa raíz: Copia a nivel de sistema de archivos de un directorio de base de datos en vivo (rsync mientras estaba en ejecución) produjo un dataset inconsistente.

Solución: Restaura desde un backup consistente o snapshot. Re-migra usando replicación o backup nativo de BD. Apaga la BD si debes hacer copias de archivos.

3) Todo está “up,” pero las escrituras fallan con permission denied

Síntoma: Logs muestran EACCES en rutas montadas; subidas fallan; la base de datos no puede escribir.

Causa raíz: Desajuste UID/GID entre host antiguo y nuevo, o cambió la propiedad del directorio de bind mount.

Solución: Alinea la propiedad al usuario del contenedor, no a tu cuenta admin. Confirma con stat y los UID de runtime del contenedor. Usa user: explícito en Compose si procede.

4) El rendimiento es la mitad de lo que era

Síntoma: Latencia aumentada tras la migración; CPU está bien; la base de datos se siente lenta.

Causa raíz: El almacenamiento se movió de SSD a disco más lento (o RAID mal configurado), opciones de filesystem difieren o diferencias en el scheduler de I/O.

Solución: Benchmark del disco en el nuevo host. Coloca el volumen DB en el dispositivo correcto. Arregla opciones de montaje. Verifica que no haya cifrado/compresión accidental que no planeaste.

5) Los clientes siguen golpeando el host antiguo intermitentemente por horas

Síntoma: Logs mixtos, comportamiento mixto; algunos usuarios ven la versión antigua.

Causa raíz: Cacheo DNS más allá del TTL, resolutores hardcodeados, conexiones de larga duración o IPs fijas en clientes.

Solución: Usa un load balancer/VIP cuando sea posible. Si es solo DNS, baja TTL con antelación y mantén el host antiguo sirviendo hasta que la cola se drene.

6) Compose up falla: “port is already allocated”

Síntoma: El nuevo host se niega a iniciar el contenedor web/proxy.

Causa raíz: Otro proceso está usando el puerto (a menudo un nginx del host, apache o un contenedor residual).

Solución: Identifica el listener con ss -lntp. Para el servicio conflictivo o ajusta puertos publicados y ruteo upstream.

7) Pérdida de datos inesperada en uploads o archivos

Síntoma: En el nuevo host faltan subidas recientes.

Causa raíz: Copia en una sola pasada; las escrituras continuaron durante la transferencia; sin sincronización final.

Solución: Rsync en dos fases: sincronización inicial en vivo, luego congelar escrituras y hacer una sincronización final, luego conmutar.

Listas de verificación / planes paso a paso

Plan 1: Downtime casi cero para stacks mayormente sin estado (blue/green + DNS/LB)

  1. Iguala el runtime: Instala Docker Engine y el plugin Compose en el nuevo host. Mantén versiones cercanas para reducir sorpresas.
  2. Provisiona almacenamiento: Crea directorios para bind mounts; planifica ubicaciones de volúmenes. Asegura suficiente disco e IOPS.
  3. Pre-pulla imágenes: Descarga todas las imágenes requeridas en el nuevo host para evitar fallos de registro el día del corte.
  4. Despliega en paralelo: Levanta el stack en el nuevo host en puertos alternos o detrás de un grupo de targets distinto del LB.
  5. Valida: Endpoints de salud, conectividad a BD, jobs en background y tareas programadas.
  6. Traffic shadowing (opcional): Mapea tráfico de solo lectura o ejecuta checks sintéticos contra el nuevo stack.
  7. Cutover: Cambia targets del LB o el registro DNS al nuevo host.
  8. Monitorea: Observa tasas de error, latencia y logs. Mantén el host antiguo listo para rollback.
  9. Plan de rollback: Si se quema el presupuesto de errores, revierte rápido. No “depures en producción” a menos que no haya otra opción.
  10. Descomisiona después: Tras una ventana estable, apaga el host antiguo y archiva configs/backups.

Plan 2: Breve downtime con snapshots consistentes (congelar + snapshot + restaurar)

  1. Prepara el nuevo host: Mismas imágenes, misma configuración, mismos secretos, misma estructura de directorios para bind mounts.
  2. Baja TTL: Si usas DNS para el corte, reduce TTL al menos 24 horas antes.
  3. Sincronización inicial: Para uploads y otros árboles de archivos, haz un rsync mientras la app está en vivo.
  4. Congela escrituras: Pon la app en modo mantenimiento o detén servicios con muchas escrituras. Confirma que no hay escrituras en curso.
  5. Toma snapshot/backup: Usa backup nativo de BD o snapshot de filesystem si está soportado.
  6. Sincronización final: Rsync de nuevo para capturar los últimos cambios.
  7. Restaura en el nuevo host: Importa la base de datos, coloca archivos, verifica la propiedad.
  8. Arranca el stack: Levanta Compose y espera a que pasen los healthchecks.
  9. Conmuta tráfico: Cambio por DNS o LB. Mantén el host antiguo detenido o en solo lectura para evitar escrituras split-brain.
  10. Descongela: Sale de modo mantenimiento y monitorea.

Plan 3: Con estado “casi sin downtime” con replicación (migración centrada en la BD)

  1. Levanta la nueva BD: Configúrala como réplica de la BD antigua. Verifica que la latencia de replicación se mantenga baja bajo carga normal.
  2. Despliega la app en el nuevo host: Apúntala a la réplica para verificación de solo lectura si es posible, o mantenla inactiva.
  3. Planifica la promoción: Decide el minuto exacto del cutover y qué harás con las escrituras (congelación breve o failover a nivel de app).
  4. Cutover: Congela escrituras brevemente, deja que la réplica se ponga al día, promueve la nueva BD a primaria.
  5. Cambia tráfico de la app: Cambia LB/DNS al nuevo host y al endpoint de la nueva BD.
  6. Mantén la BD antigua: Déjala como réplica (si es soportado) para la ventana de rollback, pero no permitas escrituras en ella.

Tres mini-historias corporativas desde el barro

Mini-historia 1: El incidente causado por una suposición equivocada (rsync como “backup”)

Una empresa SaaS mediana decidió mover un stack Compose de una VM “temporal” a un nuevo host con más CPU.
El stack era típico: Nginx, API, PostgreSQL, Redis y un worker. El equipo había hecho cortes sin estado antes, así que el plan de migración se sintió familiar:
levantar el nuevo host, rsyncear /var/lib/docker/volumes, arrancar Compose y cambiar DNS.

Ejecutaron rsync mientras todo seguía sirviendo tráfico. Tomó tiempo. Lo ejecutaron de nuevo “para ponerse al día”, se sintieron orgullosos del tamaño delta y cortaron.
El nuevo stack arrancó y parecía bien. Los usuarios iniciaron sesión. Las peticiones funcionaban. El incidente no comenzó con alarmas; empezó con tickets de soporte.

El primer síntoma fue sutil: un pequeño porcentaje de usuarios vio datos obsoletos tras actualizaciones. Luego algunos jobs en background fallaron con violaciones de constraint únicas.
Eventualmente Postgres empezó a emitir quejas relacionadas con WAL, no inmediatamente fatales pero cada vez más molestas. El equipo intentó reiniciar el contenedor de la base de datos. Ahí fue cuando se desplomó.

La suposición equivocada fue creer que rsync del directorio de datos de Postgres es “suficientemente cercano” si lo haces dos veces.
No lo es. Postgres espera un checkpoint consistente y una secuencia WAL; copiar archivos a mitad de escritura puede crear un dataset que arranca pero contiene minas.
Terminaron restaurando desde un backup lógico más antiguo y luego reparando manualmente la brecha con logs de aplicación y montones de SQL cuidadoso.

La lección que quedó: si tu plan de migración no incluye un mecanismo explícito de consistencia para la base de datos, no es un plan.
Es optimismo con una línea de comandos.

Mini-historia 2: La optimización que salió mal (almacenamiento más rápido… en papel)

Un gran equipo empresarial migró una plataforma interna alojada en Compose a hardware nuevo. La “optimización” fue consolidar el almacenamiento:
poner Docker root, volúmenes de base de datos y uploads de aplicación en un único arreglo RAID de gran capacidad.
La razón sonaba bien en la reunión: menos puntos de montaje, backups más simples y “RAID es rápido”.

Tras el cutover, todo funcionó, pero la latencia se duplicó en horas pico. La API no estaba limitada por CPU. La red estaba bien.
La base de datos tenía stalls periódicos. Los workers tardaban más en completar jobs. Los ingenieros empezaron a culpar al kernel del nuevo host, la versión de Docker e incluso la “sobrecarga de contenedores”.

Resultó que el controlador RAID estaba configurado para comportamiento orientado a capacidad con una política de caché de escritura segura pero lenta para su carga.
En el host antiguo, la base de datos vivía en SSDs de baja latencia. En el nuevo host, compartía platos con grandes escrituras secuenciales de uploads y jobs de backup.
El layout consolidado creó contención de I/O que no apareció en benchmarks sintéticos pero sí en producción.

La solución fue aburrida: separar la base de datos en almacenamiento de baja latencia y mantener el almacenamiento masivo en otro lugar, con límites de ancho de banda explícitos para copias en background.
También añadieron monitorización simple de I/O que habría hecho la causa raíz obvia el primer día.
La “optimización” no fue maliciosa; solo fue no respetar que las bases de datos no negocian con discos lentos.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día (ensayo + rollback)

Otro equipo, más pequeño y menos glamuroso, tuvo que migrar un stack Compose que manejaba aprobaciones de nómina.
El sistema no tenía mucho tráfico, pero era de alto riesgo. No podían permitirse pérdida de datos y no podían dejar a todos bloqueados por mucho tiempo.
Nadie estaba emocionado. Eso suele ser buena señal.

Hicieron un ensayo. No teórico: un ensayo real en un clon de staging con un snapshot de datos realista.
Documentaron cada comando que ejecutaron, incluyendo los que parecían obvios. Verificaron procedimientos de restauración, no solo backups.
Luego hicieron algo aún menos emocionante: escribieron un plan de rollback que incluía reversiones de DNS, orden de parada/arranque de contenedores y un punto claro de “detener el mundo”.

El día de la migración, el nuevo host arrancó pero un servicio falló por un certificado CA faltante que había estado presente silenciosamente en el host viejo.
Como habían ensayado, el fallo les resultó familiar. Arreglaron el paquete, reanudaron la secuencia de arranque y continuaron.
El downtime fue corto, predecible y explicable. Los usuarios se quejaron un poco y luego lo olvidaron.

El día fue “exitoso” porque lo trataron como un cambio con bordes filosos: ensayo, validación, rollback.
Nada heroico. Nada ingenioso. Solo competencia.

Preguntas frecuentes

1) ¿Puedo migrar un stack Compose con literalmente cero tiempo de inactividad?

Si el stack es sin estado, casi sí. Si tiene estado, “literalmente cero” suele requerir replicación o una capa de datos externalizada.
La mayoría de equipos puede lograr “sin ventana de mantenimiento” con un pequeño blip, pero no cero en el sentido matemático.

2) ¿Es seguro copiar volúmenes nombrados de Docker con rsync?

Para datos estáticos o datos que puedes quietar, sí. Para bases de datos en vivo, no. Si debes copiar, detén el servicio o usa snapshots, o herramientas nativas de backup/replicación de la BD.

3) ¿Debería copiar /var/lib/docker al nuevo host?

Evítalo a menos que tengas una razón fuerte y un entorno de prueba. Te acopla a detalles del driver de almacenamiento, internals de Docker y compatibilidad de versiones.
Prefiere migrar datos de aplicación y redeplegar contenedores limpiamente.

4) ¿Cuál es el mecanismo de cutover más seguro: DNS, load balancer o VIP?

Cambios en targets de load balancer y movimientos de VIP suelen ser los más rápidos de revertir y los menos dependientes del comportamiento del cliente.
DNS funciona, pero debes planificar cacheo y conexiones de larga duración.

5) ¿Cómo manejo certificados TLS durante la migración?

Trátalos como estado de primera clase. Inventaria dónde viven (bind mounts, archivos de secretos).
Cópialos de forma segura, verifica permisos de archivo y valida la cadena completa en el nuevo host antes del cutover.

6) ¿Necesito mantener las mismas IPs de contenedor o nombres de red?

No deberías depender de IPs de contenedor en absoluto. Usa nombres de servicio en la red de Compose.
Los nombres de red importan solo si sistemas externos los referencian (raro). La mayoría de las veces, un nombre de proyecto estable es suficiente.

7) ¿Cómo migrar uploads u otros archivos mutables?

Haz una sincronización en dos fases: copia en vivo y luego congela escrituras brevemente para un rsync final.
Si puedes, migra uploads a almacenamiento de objetos y deja de preocuparte por archivos en host para siempre.

8) ¿Qué pasa con workers en background y tareas programadas durante el cutover?

Páralos o asegúrate de idempotencia. Durante el cutover, workers duplicados pueden procesar tareas dos veces, y así es como los sistemas financieros inventan formas nuevas de emoción.
Si no puedes pausar, diseña deduplicación y locks de trabajo.

9) ¿Debo actualizar Docker/Compose durante la migración?

Evita combinar una migración de host con una actualización del runtime a menos que tengas tiempo para probar.
Si debes actualizar, hazlo intencionalmente con ensayo y validación, no como un efecto secundario de “construir servidor nuevo”.

Conclusión: pasos prácticos siguientes

Si te llevas una cosa de esto: las migraciones de Compose fallan cuando los equipos tratan el estado como un detalle de implementación.
Define qué significa “sin tiempo de inactividad” en términos del negocio y luego elige un modelo de migración que coincida con la realidad de tus datos.
Replica cuando puedas. Haz snapshots cuando debas. Congela escrituras cuando no tengas mejores opciones. No rsyncees bases de datos en vivo y lo llames ingeniería.

Pasos siguientes que rinden inmediatamente:

  • Ejecuta las tareas de inventario arriba en el host antiguo y anota qué es stateful.
  • Elige un mecanismo de conmutación con un palanca de rollback que confíes (LB/VIP mejor que solo DNS).
  • Rehearsa la migración en un clon de staging, incluyendo restauración y rollback.
  • Añade healthchecks y gating de readiness para que “up” signifique “listo”.
  • Haz explícita la colocación de almacenamiento en el nuevo host; las bases de datos no pertenecen en discos misteriosos.

Las migraciones no son glamorosas. Son suero de la verdad operacional. Hazlo como para poder dormir después.

← Anterior
Fundamentos de SR-IOV en Debian 13: por qué falla y cómo depurar la primera vez
Siguiente →
Sustituir vCenter por Proxmox: qué ganas, qué pierdes y soluciones que realmente funcionan

Deja un comentario