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ónAcción recomendada
La nueva versión no arranca o no pasa el health checkRevertir de inmediato (automático)
Errores 5xx o latencia disparada tras el despliegueRevertir y analizar después
Fallo de seguridad introducido por la nueva versiónRevertir
Bug menor con solución sencilla y sin impacto en datosCorregir hacia delante con un nuevo despliegue
La nueva versión ya escribió datos con un formato que la anterior no entiendeCorregir 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:

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:

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:

nano /opt/miapp/compose.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:

nano /opt/miapp/.env
APP_VERSION=1.4.1
DB_PASSWORD=tu_contraseña_segura
chmod 600 /opt/miapp/.env

Arranca la aplicación y comprueba la versión en marcha:

docker compose -f /opt/miapp/compose.yaml up -d
docker compose -f /opt/miapp/compose.yaml images app
CONTAINER       REPOSITORY                 TAG     IMAGE ID       SIZE
miapp-app-1     ghcr.io/tu_usuario/miapp   1.4.1   3f1c2a9d8e7b   152MB
curl -fsS http://127.0.0.1:8080/health
{"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:

sudo nano /usr/local/bin/desplegar-miapp
#!/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 <version>}"
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:

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:

desplegar-miapp 1.4.2
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:

desplegar-miapp 1.4.3-rota; echo "Código de salida: $?"

Tras unos 60 segundos verás:

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:

grep APP_VERSION /opt/miapp/.env
cat /opt/miapp/despliegues.log
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:

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.

    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.

    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:

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

Vuelve a arrancar la versión anterior:

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:

crontab -e
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:

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:

kubectl rollout status deployment/miapp --timeout=120s

Consulta el historial y revierte a la revisión anterior o a una concreta:

kubectl rollout history deployment/miapp
deployment.apps/miapp
REVISION  CHANGE-CAUSE
3         Desplegar 1.4.2
4         Desplegar 1.4.3
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:

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.