--- title: "Cómo migrar contenedores Docker a otro servidor" description: "Mueve una aplicación Docker Compose a un servidor nuevo con Ubuntu 24.04: copia el proyecto, las imágenes locales y los volúmenes, y comprueba que funciona." category: "Migración de Servidores" language: es source: https://cubepath.com/es/docs/migración-de-servidores/migracion-de-contenedores-docker updated: 2026-09-25 --- # Cómo migrar contenedores Docker a otro servidor Un contenedor Docker no se "mueve" de un servidor a otro: lo que se migra es lo que lo define (el archivo de Compose, las variables de entorno y la imagen) y los datos que guarda en volúmenes. En este tutorial migrarás una aplicación gestionada con Docker Compose, con volúmenes con nombre y directorios montados, desde un servidor antiguo a uno nuevo con Ubuntu 24.04. Al terminar, la aplicación funcionará en el destino con los mismos datos y podrás apagar el origen. ## Requisitos previos - El servidor de origen con Docker y el plugin de Docker Compose (`docker compose`). - Un servidor de destino con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con Docker Engine y Docker Compose instalados desde el repositorio oficial de Docker. - Un usuario no root con `sudo` en ambos servidores, miembro del grupo `docker`, y acceso SSH por clave desde el origen al destino. - Espacio libre en disco en ambos servidores para al menos una copia comprimida de los volúmenes. En esta guía, `your_user` es tu usuario, `new_server_ip` la IP del destino, `/opt/myapp` el directorio del proyecto de Compose y `myapp` el nombre del proyecto. Sustitúyelos por tus valores. > Nota: si la aplicación incluye una base de datos, además de copiar su volumen conviene hacer un volcado lógico (`pg_dump`, `mysqldump`) antes de empezar. Si la copia del volumen falla por cualquier motivo, podrás restaurar desde el volcado. ## Paso 1: Inventariar lo que hay que migrar En el **origen**, lista los proyectos de Compose y sus contenedores: ```bash docker compose ls docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' ``` ```output NAME STATUS CONFIG FILES myapp running(3) /opt/myapp/compose.yaml ``` Para cada contenedor, revisa qué datos persistentes usa. Este comando muestra el tipo de montaje (`volume` o `bind`), su origen y dónde se monta: ```bash docker inspect --format '{{.Name}}{{range .Mounts}} {{.Type}} {{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}} -> {{.Destination}}{{end}}' $(docker ps -q) ``` ```output /myapp-db-1 volume myapp_dbdata -> /var/lib/postgresql/data /myapp-web-1 bind /opt/myapp/uploads -> /app/uploads ``` Hay dos tipos de datos que migrar: - **Volúmenes con nombre** (`myapp_dbdata`): Compose les añade el nombre del proyecto como prefijo. Viven en `/var/lib/docker/volumes/` y se copian con un contenedor auxiliar. - **Montajes de directorio** (`/opt/myapp/uploads`): son directorios normales del servidor y se copian con `rsync`. Anota también qué imágenes se construyen localmente (`build:` en el archivo de Compose) y cuáles vienen de un registro (`image:`). Las segundas se descargarán solas en el destino. Comprueba la arquitectura de las imágenes. Si el origen es x86_64 (`amd64`) y el destino es ARM (`arm64`), o al revés, tendrás que reconstruir las imágenes locales en el destino en lugar de copiarlas: ```bash docker image inspect --format '{{.RepoTags}} {{.Architecture}}' $(docker images -q) ``` ## Paso 2: Copiar el proyecto de Compose El directorio del proyecto contiene `compose.yaml` (o `docker-compose.yml`), el archivo `.env` con las variables y cualquier archivo de configuración montado. En el **destino**, crea el directorio y dale permisos a tu usuario: ```bash sudo mkdir -p /opt/myapp sudo chown your_user:your_user /opt/myapp ``` Desde el **origen**, copia el proyecto. Esta primera copia incluye los directorios montados como `uploads`, y se puede hacer con la aplicación en marcha: ```bash rsync -aHAX --info=progress2 /opt/myapp/ your_user@new_server_ip:/opt/myapp/ ``` Si algún archivo del proyecto pertenece a root o a otro usuario (algo habitual en directorios donde escribe un contenedor), ejecuta rsync con `sudo` en ambos lados. En el destino, permite antes a tu usuario ejecutar `rsync` con sudo sin contraseña creando `/etc/sudoers.d/rsync-migracion` con la línea `your_user ALL=(root) NOPASSWD: /usr/bin/rsync`, y lanza desde el origen: ```bash sudo rsync -aHAX --numeric-ids --info=progress2 \ -e "ssh -i /home/your_user/.ssh/id_ed25519" \ --rsync-path="sudo rsync" \ /opt/myapp/ your_user@new_server_ip:/opt/myapp/ ``` `--numeric-ids` conserva los UID y GID originales, que son los que esperan los procesos de dentro de los contenedores. En el destino, valida el archivo de Compose. Si no hay errores, el comando muestra la configuración completa ya interpretada: ```bash cd /opt/myapp docker compose config --quiet && echo "Compose OK" ``` ## Paso 3: Transferir las imágenes construidas localmente Las imágenes que vienen de un registro (Docker Hub, GHCR o uno privado) no hace falta copiarlas: `docker compose` las descargará en el destino. Si usas un registro privado, inicia sesión en el destino con `docker login`. Las imágenes construidas en el origen tienes dos formas de llevarlas: reconstruirlas en el destino con `docker compose build`, si el código fuente está en el directorio del proyecto, o transferirlas tal cual. Para transferir una imagen sin crear archivos intermedios, envía la salida de `docker save` por SSH directamente a `docker load` en el destino: ```bash docker save myapp-web:latest | gzip | ssh your_user@new_server_ip 'gunzip | docker load' ``` ```output Loaded image: myapp-web:latest ``` Comprueba en el destino que la imagen está disponible: ```bash docker image ls myapp-web ``` > Nota: `docker save` guarda la imagen con todas sus capas y etiquetas. No confundas con `docker export`, que vuelca solo el sistema de archivos de un contenedor y pierde el historial, los puertos expuestos, el comando de inicio y las variables de entorno de la imagen. Para migrar, usa siempre imágenes, no contenedores exportados. ## Paso 4: Crear los volúmenes vacíos en el destino Para restaurar los datos, primero deben existir los volúmenes con el nombre exacto que usará Compose. En el **destino**, crea los volúmenes, las redes y los contenedores del proyecto sin arrancarlos: ```bash cd /opt/myapp docker compose up --no-start ``` Comprueba que los volúmenes existen con el mismo nombre que en el origen: ```bash docker volume ls --filter name=myapp_ ``` ```output DRIVER VOLUME NAME local myapp_dbdata ``` ## Paso 5: Detener la aplicación en el origen y copiar los volúmenes Copiar el volumen de una base de datos mientras está escribiendo produce una copia inconsistente, así que este paso requiere parar la aplicación. Es la ventana de corte: todo lo anterior se ha hecho con el servicio funcionando. En el **origen**, detén los contenedores sin eliminarlos, para poder volver a arrancarlos si algo falla: ```bash cd /opt/myapp docker compose stop ``` Copia cada volumen con nombre al destino. Un contenedor temporal de Alpine monta el volumen en modo solo lectura y lo empaqueta con `tar`; la salida viaja por SSH a otro contenedor temporal en el destino que la desempaqueta dentro del volumen vacío: ```bash docker run --rm -v myapp_dbdata:/data:ro alpine tar -C /data -czf - . \ | ssh your_user@new_server_ip 'docker run --rm -i -v myapp_dbdata:/data alpine tar -C /data -xzf -' ``` `tar` conserva propietarios y permisos al ejecutarse como root dentro del contenedor. Repite el comando para cada volumen de la lista del paso 1. Si subiste archivos a directorios montados después de la primera copia, sincroniza de nuevo el proyecto. `--delete` elimina en el destino lo que se borró en el origen: ```bash rsync -aHAX --delete --info=progress2 /opt/myapp/ your_user@new_server_ip:/opt/myapp/ ``` Usa la variante con `sudo` del paso 2 si la necesitaste entonces. Compara el tamaño de los datos en ambos servidores para detectar una copia incompleta: ```bash docker run --rm -v myapp_dbdata:/data:ro alpine du -s /data ``` Ejecuta el mismo comando en el origen y en el destino; los valores deben coincidir o ser muy parecidos. ## Paso 6: Arrancar la aplicación en el destino En el **destino**, arranca el proyecto: ```bash cd /opt/myapp docker compose up -d ``` Comprueba que todos los servicios están en marcha y, si tienen `healthcheck`, que aparecen como `healthy`: ```bash docker compose ps ``` ```output NAME IMAGE SERVICE STATUS myapp-db-1 postgres:16 db Up 20 seconds (healthy) myapp-web-1 myapp-web:latest web Up 18 seconds ``` Revisa los logs en busca de errores de permisos o de conexión con la base de datos: ```bash docker compose logs --tail 50 ``` Prueba la aplicación contra la IP nueva antes de tocar los DNS. Si publica el puerto 80, por ejemplo: ```bash curl -I -H "Host: your_domain" http://new_server_ip ``` Una respuesta `HTTP/1.1 200 OK` (o la redirección que devuelva normalmente tu aplicación) confirma que funciona. Comprueba también que ves los datos esperados: usuarios, archivos subidos, registros recientes. Asegúrate de que el cortafuegos del destino permite los puertos publicados, por ejemplo con `sudo ufw allow 80,443/tcp`. Ten en cuenta que Docker añade sus propias reglas de iptables para los puertos publicados, por lo que UFW no bloquea los puertos que publiques en todas las interfaces (`0.0.0.0`). Si un servicio solo debe ser accesible localmente, publícalo en `127.0.0.1` en el archivo de Compose. ## Paso 7: Cambiar el DNS y retirar el origen Cuando la aplicación funcione en el destino, apunta los registros A y AAAA de tu dominio a `new_server_ip`. Si bajaste el TTL a 300 segundos el día anterior, el cambio se propagará en pocos minutos. Verifícalo: ```bash dig +short A your_domain @1.1.1.1 ``` Si la aplicación obtiene certificados TLS automáticamente (Caddy, Traefik o un contenedor de Certbot), el destino podrá renovarlos en cuanto el DNS apunte a él. Si copiaste el volumen con los certificados, no habrá ninguna interrupción de HTTPS. Mantén el origen con los contenedores detenidos durante unos días. Si necesitas volver atrás, basta con revertir el DNS y ejecutar `docker compose start` en el origen. Cuando estés seguro, elimínalos allí con: ```bash docker compose down ``` Este comando no borra los volúmenes con nombre; para eliminarlos también añade `--volumes`, solo cuando ya no necesites los datos del origen. Elimina también el archivo `/etc/sudoers.d/rsync-migracion` del destino si lo creaste. ## Solución de problemas - **`Permission denied` en los logs tras arrancar**: los archivos de un directorio montado cambiaron de propietario al copiarlos sin `sudo` ni `--numeric-ids`. Repite la copia con la variante con `sudo` del paso 2. - **`exec format error` al arrancar un contenedor**: la imagen es de otra arquitectura. Reconstrúyela en el destino con `docker compose build`. - **La base de datos arranca vacía**: el volumen del destino no se llama igual que el del origen, normalmente porque el directorio del proyecto tiene otro nombre y Compose usó otro prefijo. Comprueba `docker volume ls` y fija el nombre del proyecto con `name: myapp` al principio de `compose.yaml`. - **PostgreSQL o MySQL no arrancan tras la restauración**: la imagen del destino es de otra versión mayor que la del origen. Usa exactamente la misma etiqueta de imagen y actualiza la versión después, por separado. ## Conclusión Has migrado una aplicación Docker Compose completa: el proyecto y los directorios montados con `rsync`, las imágenes locales con `docker save` y `docker load`, y los volúmenes con `tar` dentro de contenedores temporales, con una única ventana de corte para la copia final. Como siguientes pasos, publica tus imágenes en un registro para que la próxima migración sea un simple `docker compose pull`, y programa copias de seguridad periódicas de los volúmenes del servidor nuevo.