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/healthque devuelva200solo cuando la aplicación está operativa (incluida su conexión a la base de datos). curlinstalado (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:
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 pullfalla,set -edetiene 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 elsedo se interprete como otra cosa. - El script devuelve
1si revirtió y2si ni siquiera la versión anterior responde, para que el pipeline o quien lo ejecute sepa qué ha pasado. despliegues.logguarda 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:
-
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; -
Despliegue B: el código lee de la columna nueva y sigue escribiendo en ambas. Revertir a A es seguro.
-
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
Advertenciarestaurar 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_dumppor si necesitas recuperar registros concretos.
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.