--- title: "Cómo revertir despliegues fallidos con Docker Compose" description: "Prepara despliegues reversibles en Ubuntu 24.04: imágenes versionadas, rollback automático por health check, copia de PostgreSQL previa y kubectl rollout undo." category: "CI/CD DevOps" language: es source: https://cubepath.com/es/docs/ci-cd-devops/estrategias-de-reversion-para-implementaciones-fallidas updated: 2026-09-25 --- # Cómo revertir despliegues fallidos con Docker Compose Un despliegue que falla no es un problema si puedes volver a la versión anterior en segundos. Para eso la reversión (*rollback*) tiene que estar preparada antes del despliegue: imágenes con versiones inmutables, un registro de qué versión estaba en marcha, una comprobación de salud que decida si el despliegue es bueno y una copia de la base de datos por si una migración sale mal. En este tutorial montarás todo eso para una aplicación con Docker Compose en Ubuntu 24.04, con un script que despliega y revierte automáticamente, y verás el equivalente en Kubernetes. ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con `sudo`. - Docker Engine y el plugin Docker Compose instalados, con tu usuario en el grupo `docker`. - Una aplicación publicada como imagen en un registro (en los ejemplos, `ghcr.io/tu_usuario/miapp`) con al menos dos versiones etiquetadas, y que exponga un endpoint de salud como `/health` que devuelva `200` solo cuando la aplicación está operativa (incluida su conexión a la base de datos). - `curl` instalado (`sudo apt install -y curl`). ## Cuándo revertir y cuándo corregir hacia delante No todo fallo se arregla revirtiendo. Antes de automatizar nada, conviene tener claro el criterio: | Situación | Acción recomendada | |-----------|--------------------| | La nueva versión no arranca o no pasa el health check | Revertir de inmediato (automático) | | Errores 5xx o latencia disparada tras el despliegue | Revertir y analizar después | | Fallo de seguridad introducido por la nueva versión | Revertir | | Bug menor con solución sencilla y sin impacto en datos | Corregir hacia delante con un nuevo despliegue | | La nueva versión ya escribió datos con un formato que la anterior no entiende | Corregir hacia delante: revertir el código no deshace los datos | El último caso es el que más duele y se evita diseñando migraciones compatibles hacia atrás, como verás en el paso 5. ## Paso 1: Usar etiquetas de imagen inmutables Para revertir tienes que poder nombrar con exactitud la versión anterior. Si despliegas siempre `miapp:latest`, la versión anterior ya no existe con ese nombre. Etiqueta cada imagen con su versión o con el SHA del commit, y no reutilices nunca una etiqueta: ```bash docker build -t ghcr.io/tu_usuario/miapp:1.4.2 . docker push ghcr.io/tu_usuario/miapp:1.4.2 ``` En un pipeline, `$GITHUB_SHA` o `git rev-parse --short HEAD` sirven como etiqueta única. Si tu registro permite marcar etiquetas como inmutables (Harbor, Amazon ECR y otros lo admiten), actívalo: así nadie puede sobrescribir `1.4.2` con otro contenido. Configura también la política de limpieza del registro para que conserve, como mínimo, las últimas cinco o diez versiones. ## Paso 2: Parametrizar la versión en Docker Compose Crea el directorio de la aplicación y dale la propiedad a tu usuario: ```bash sudo mkdir -p /opt/miapp/backups sudo chown -R "$USER:$USER" /opt/miapp ``` Crea el archivo `/opt/miapp/compose.yaml`. La versión de la imagen sale de la variable `APP_VERSION`; si no está definida, Compose se niega a arrancar en lugar de usar una versión por defecto: ```bash nano /opt/miapp/compose.yaml ``` ```yaml services: app: image: ghcr.io/tu_usuario/miapp:${APP_VERSION:?APP_VERSION no definida} restart: unless-stopped ports: - "127.0.0.1:8080:8080" environment: DATABASE_URL: postgresql://app:${DB_PASSWORD}@db:5432/app depends_on: db: condition: service_healthy db: image: postgres:16 restart: unless-stopped environment: POSTGRES_USER: app POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD no definida} POSTGRES_DB: app volumes: - db-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U app -d app"] interval: 5s timeout: 5s retries: 10 volumes: db-data: ``` Compose lee automáticamente el archivo `.env` del mismo directorio. Créalo con la versión inicial y una contraseña propia en lugar de `tu_contraseña_segura`: ```bash nano /opt/miapp/.env ``` ```ini APP_VERSION=1.4.1 DB_PASSWORD=tu_contraseña_segura ``` ```bash chmod 600 /opt/miapp/.env ``` Arranca la aplicación y comprueba la versión en marcha: ```bash docker compose -f /opt/miapp/compose.yaml up -d docker compose -f /opt/miapp/compose.yaml images app ``` ```output CONTAINER REPOSITORY TAG IMAGE ID SIZE miapp-app-1 ghcr.io/tu_usuario/miapp 1.4.1 3f1c2a9d8e7b 152MB ``` ```bash curl -fsS http://127.0.0.1:8080/health ``` ```output {"status":"ok"} ``` El archivo `.env` es ahora la fuente de verdad de la versión desplegada: cambiarla y ejecutar `up -d` es todo lo que hace falta para desplegar o revertir. ## Paso 3: Crear un script de despliegue con reversión automática El script hace, en orden: valida la versión solicitada, anota la versión actual, copia la base de datos, despliega la nueva versión y espera a que el health check responda. Si no responde en 60 segundos, vuelve a la versión anterior y termina con error, de forma que el pipeline que lo lanzó también falle. Crea el archivo `/usr/local/bin/desplegar-miapp`: ```bash sudo nano /usr/local/bin/desplegar-miapp ``` ```bash #!/usr/bin/env bash set -euo pipefail APP_DIR=/opt/miapp IMAGE=ghcr.io/tu_usuario/miapp COMPOSE=(docker compose -f "$APP_DIR/compose.yaml") ENV_FILE="$APP_DIR/.env" HISTORIAL="$APP_DIR/despliegues.log" HEALTH_URL=http://127.0.0.1:8080/health nueva="${1:?Uso: desplegar-miapp }" if [[ ! "$nueva" =~ ^[A-Za-z0-9._-]+$ ]]; then echo "Versión no válida: $nueva" >&2 exit 1 fi actual="$(sed -n 's/^APP_VERSION=//p' "$ENV_FILE")" if [[ "$nueva" == "$actual" ]]; then echo "La versión $nueva ya está desplegada" exit 0 fi usar_version() { sed -i "s/^APP_VERSION=.*/APP_VERSION=$1/" "$ENV_FILE" "${COMPOSE[@]}" pull app "${COMPOSE[@]}" up -d app } esperar_salud() { for _ in $(seq 1 30); do if curl -fsS --max-time 2 "$HEALTH_URL" > /dev/null 2>&1; then return 0 fi sleep 2 done return 1 } # Descargar antes de tocar nada: si la imagen no existe, no se para el servicio docker pull "$IMAGE:$nueva" > /dev/null copia="$APP_DIR/backups/antes-de-$nueva-$(date +%Y%m%d-%H%M%S).dump" echo "Copiando la base de datos en $copia" "${COMPOSE[@]}" exec -T db pg_dump -U app -Fc app > "$copia" echo "Desplegando $nueva (versión actual: $actual)" usar_version "$nueva" if esperar_salud; then echo "$(date -Is) $actual -> $nueva OK" >> "$HISTORIAL" echo "Despliegue de $nueva correcto" exit 0 fi echo "El health check falla con $nueva, volviendo a $actual" >&2 usar_version "$actual" if esperar_salud; then echo "$(date -Is) $actual -> $nueva REVERTIDO" >> "$HISTORIAL" echo "Reversión a $actual completada" >&2 exit 1 fi echo "$(date -Is) $actual -> $nueva FALLO_TOTAL" >> "$HISTORIAL" echo "La versión $actual tampoco responde, revisa: ${COMPOSE[*]} logs app" >&2 exit 2 ``` Sustituye el valor de `IMAGE` por tu ruta de imagen, la misma que en `compose.yaml`. Hazlo ejecutable: ```bash sudo chmod 755 /usr/local/bin/desplegar-miapp ``` Algunas decisiones del script que merece la pena entender: - La imagen nueva se descarga **antes** de parar nada. Si la etiqueta no existe, `docker pull` falla, `set -e` detiene el script y la versión actual sigue funcionando. - La validación de la versión con una expresión regular impide que un valor con `/` o espacios rompa el `sed` o se interprete como otra cosa. - El script devuelve `1` si revirtió y `2` si ni siquiera la versión anterior responde, para que el pipeline o quien lo ejecute sepa qué ha pasado. - `despliegues.log` guarda el historial: sabes en todo momento qué versión estaba antes de cada cambio. ## Paso 4: Probar el despliegue y la reversión Despliega una versión buena: ```bash desplegar-miapp 1.4.2 ``` ```output Copiando la base de datos en /opt/miapp/backups/antes-de-1.4.2-20260925-101500.dump Desplegando 1.4.2 (versión actual: 1.4.1) Despliegue de 1.4.2 correcto ``` Ahora simula una versión rota. Publica una imagen que no responda al health check (por ejemplo, una compilación con un error de configuración) con la etiqueta `1.4.3-rota` y despliégala: ```bash desplegar-miapp 1.4.3-rota; echo "Código de salida: $?" ``` Tras unos 60 segundos verás: ```output Copiando la base de datos en /opt/miapp/backups/antes-de-1.4.3-rota-20260925-102010.dump Desplegando 1.4.3-rota (versión actual: 1.4.2) El health check falla con 1.4.3-rota, volviendo a 1.4.2 Reversión a 1.4.2 completada Código de salida: 1 ``` Comprueba el estado final y el historial: ```bash grep APP_VERSION /opt/miapp/.env cat /opt/miapp/despliegues.log ``` ```output APP_VERSION=1.4.2 2026-09-25T10:15:12+00:00 1.4.1 -> 1.4.2 OK 2026-09-25T10:21:14+00:00 1.4.2 -> 1.4.3-rota REVERTIDO ``` Para una reversión manual (por ejemplo, un bug que el health check no detecta), basta con desplegar la versión anterior con el mismo script: ```bash desplegar-miapp 1.4.1 ``` Un rollback que nunca se ha probado no es un rollback. Repite esta prueba en tu entorno de staging cada vez que cambies el proceso de despliegue. ## Paso 5: Hacer reversibles los cambios de base de datos Revertir la imagen no revierte el esquema. Si la versión 1.4.3 renombra una columna y la 1.4.2 todavía la busca con el nombre antiguo, volver a la 1.4.2 no arreglará nada. La técnica que lo evita se llama **expandir y contraer**: cada cambio de esquema se reparte en varios despliegues de forma que la versión anterior del código siga funcionando con el esquema nuevo. Por ejemplo, para renombrar `clientes.nombre` a `clientes.nombre_completo`: 1. **Despliegue A (expandir)**: añade la columna nueva y copia los datos. El código escribe en ambas columnas y lee de la antigua. ```sql ALTER TABLE clientes ADD COLUMN nombre_completo text; UPDATE clientes SET nombre_completo = nombre WHERE nombre_completo IS NULL; ``` 2. **Despliegue B**: el código lee de la columna nueva y sigue escribiendo en ambas. Revertir a A es seguro. 3. **Despliegue C (contraer)**: cuando B lleva días estable, el código deja de usar la columna antigua y una migración posterior la elimina. ```sql ALTER TABLE clientes DROP COLUMN nombre; ``` En cada paso, la versión anterior es compatible con el esquema actual, así que el rollback del paso 3 siempre es seguro. Las operaciones destructivas (`DROP`, cambios de tipo, `NOT NULL` sobre columnas existentes) van siempre en un despliegue posterior y separado. Si aun así una migración corrompe datos, tienes la copia que el script hizo justo antes de desplegar. Para restaurarla, para la aplicación y restaura con `pg_restore`: ```bash docker compose -f /opt/miapp/compose.yaml stop app docker compose -f /opt/miapp/compose.yaml exec -T db \ pg_restore -U app -d app --clean --if-exists \ < /opt/miapp/backups/antes-de-1.4.3-rota-20260925-102010.dump ``` > Advertencia: restaurar la copia descarta todo lo escrito desde que se hizo. Úsala solo cuando la corrupción sea peor que perder esos datos, y guarda antes una copia del estado actual con `pg_dump` por si necesitas recuperar registros concretos. Vuelve a arrancar la versión anterior: ```bash desplegar-miapp 1.4.2 ``` Las copias previas al despliegue ocupan espacio. Programa su limpieza con cron conservando, por ejemplo, las de los últimos 14 días: ```bash crontab -e ``` ```text 30 3 * * * find /opt/miapp/backups -name '*.dump' -mtime +14 -delete ``` Estas copias no sustituyen a las copias de seguridad programadas fuera del servidor: solo cubren el caso de "la migración de hoy salió mal". ## Paso 6: Revertir en Kubernetes Si despliegas en Kubernetes, el propio Deployment guarda las revisiones anteriores (10 por defecto, según `revisionHistoryLimit`) y hace la reversión por ti. Anota el motivo de cada cambio para que el historial sea legible: ```bash kubectl set image deployment/miapp app=ghcr.io/tu_usuario/miapp:1.4.3 kubectl annotate deployment/miapp kubernetes.io/change-cause="Desplegar 1.4.3" --overwrite ``` Espera a que termine el despliegue con un tiempo límite. Si las sondas `readinessProbe` no pasan, Kubernetes no retira los pods antiguos y el comando falla al agotar el tiempo: ```bash kubectl rollout status deployment/miapp --timeout=120s ``` Consulta el historial y revierte a la revisión anterior o a una concreta: ```bash kubectl rollout history deployment/miapp ``` ```output deployment.apps/miapp REVISION CHANGE-CAUSE 3 Desplegar 1.4.2 4 Desplegar 1.4.3 ``` ```bash kubectl rollout undo deployment/miapp kubectl rollout undo deployment/miapp --to-revision=3 ``` En un pipeline, encadena ambos comandos para revertir automáticamente si el despliegue no se completa: ```bash kubectl rollout status deployment/miapp --timeout=120s || { kubectl rollout undo deployment/miapp exit 1 } ``` Las mismas reglas del paso 5 sobre migraciones se aplican aquí: `rollout undo` solo revierte los pods, no la base de datos. ## Solución de problemas **`APP_VERSION no definida` al ejecutar Compose**: ejecutas `docker compose` sin `-f /opt/miapp/compose.yaml` desde otro directorio, o el archivo `.env` no está junto a `compose.yaml`. **El script revierte aunque la aplicación funciona**: la aplicación tarda más de 60 segundos en arrancar. Aumenta el número de intentos en `esperar_salud` o revisa por qué el arranque es tan lento. **`pg_restore: error: could not execute query: ERROR: must be owner of ...`**: estás restaurando con un usuario distinto del propietario de los objetos. Usa el mismo usuario con el que se hizo la copia (`-U app`). **`kubectl rollout undo` no cambia nada**: si la revisión anterior tiene la misma plantilla de pod, no hay nada que revertir. Comprueba con `kubectl rollout history deployment/miapp --revision=N` qué imagen tiene cada revisión. ## Conclusión Has preparado despliegues reversibles: imágenes con etiquetas inmutables, una versión desplegada definida en un único archivo, un script que copia la base de datos, despliega, comprueba la salud y revierte solo si algo falla, y migraciones diseñadas para que volver atrás sea seguro. Como siguientes pasos, puedes llamar a `desplegar-miapp` desde tu pipeline de CI/CD por SSH, añadir una prueba de humo más completa que el endpoint `/health`, o reducir aún más el riesgo con despliegues *blue-green* detrás de un balanceador, donde revertir es solo cambiar el destino del tráfico.