Problema

En entornos donde varios contenedores se ejecutan de forma continua, mantener las imágenes actualizadas es una tarea rutinaria pero delicada. La mayoría de los administradores confían en herramientas que descargan la última versión, recrean el contenedor y continúan con la operación. El punto crítico ocurre cuando la nueva imagen contiene errores de inicio, fallos de dependencias o configuraciones que hacen que el contenedor nunca alcance un estado saludable. En esos casos el proceso de actualización deja el servicio inoperativo hasta que se detecta manualmente el problema y se revierte a la versión anterior. El tiempo de inactividad, aunque breve, puede romper flujos de CI/CD, afectar usuarios finales y generar alertas innecesarias.

El patrón que se repite es: actualización automática → contenedor no pasa healthcheck → falta de mecanismo de rollback → interrupción del servicio. La ausencia de una verificación post‑despliegue hace que el proceso sea “ciego” y que la única señal de fallo sea la caída del propio contenedor.

Causa

  1. Dependencia exclusiva del pull‑and‑replace
    Herramientas que solo sustituyen la imagen sin validar la ejecución posterior asumen que la nueva versión es siempre compatible. Cuando la imagen rompe la cadena de arranque (por ejemplo, cambios en variables de entorno o scripts de migración que fallan), el contenedor nunca llega a estar “healthy”.

  2. Healthcheck ausente o insuficiente
    Si el Dockerfile no define un HEALTHCHECK, el motor no tiene forma de saber si el proceso interno está listo. En esos casos el auto‑updater no tiene un criterio objetivo para decidir si la actualización fue exitosa.

  3. Falta de control de tiempo
    Algunas actualizaciones pueden tardar varios segundos o minutos en iniciar. Un proceso que solo verifica que el contenedor está “running” puede marcar la actualización como exitosa antes de que el servicio realmente responda.

  4. Migraciones y scripts de inicialización
    Los contenedores que ejecutan migraciones de base de datos o scripts de configuración al arrancar pueden fallar y terminar con un código de salida distinto de cero. Sin una política de rollback, el contenedor queda detenido y el servicio desaparece.

  5. Etiquetado y alcance de la actualización
    Aplicar la actualización a todos los contenedores indiscriminadamente aumenta la superficie de riesgo. Falta de filtros (por ejemplo, etiquetas específicas) obliga a actualizar servicios que no necesitan cambios frecuentes.

Solución

Un enfoque robusto combina tres pilares:

  1. Actualizador con verificación de healthcheck
    El proceso debe:

    • Descargar la nueva imagen.
    • Crear un contenedor temporal con la misma configuración.
    • Esperar a que el HEALTHCHECK reporte healthy dentro de un plazo configurable (p.ej., 120 s).
    • Si el contenedor pasa la verificación, sustituir el contenedor original.
    • Si no pasa, eliminar el contenedor temporal y volver a iniciar el contenedor anterior.
  2. Rollback automático
    Mantener la versión anterior disponible (por ejemplo, usando la etiqueta :previous o guardando el ID de la imagen) permite revertir en segundos. El rollback debe ejecutarse antes de que el contenedor original sea detenido definitivamente.

  3. Control por etiquetas y políticas de frecuencia

    • Usa una etiqueta como freshdock.enable=true para marcar los contenedores que pueden ser actualizados.
    • Define frecuencias (live, nightly, weekly, monthly) mediante etiquetas o variables de entorno.
    • Permite sobrescribir la frecuencia con un cron externo cuando sea necesario.

Implementación práctica

Una solución ligera se puede empaquetar como un binario estático (≈10 MB) que se ejecuta dentro de un contenedor dedicado. El contenedor solo necesita acceso de solo‑lectura al socket de Docker (/var/run/docker.sock:ro). La lógica del actualizador se escribe en Rust, Go o cualquier lenguaje con buen soporte para la API de Docker (v1.44 o superior). Los pasos esenciales son:

  1. Descubrimiento de contenedores
    Listar todos los contenedores y filtrar por la etiqueta freshdock.enable=true.

  2. Planificación según política
    Evaluar la etiqueta de frecuencia y comparar con la última ejecución (almacenada en un archivo de estado).

  3. Proceso de actualización

    • docker pull <image:tag>
    • docker create --name <temp> --label freshdock.temp=true <options>
    • docker start <temp>
    • Esperar a que docker inspect --format '{{.State.Health.Status}}' <temp> devuelva healthy o alcance el timeout.
    • Si es healthy: docker rename <old> <old>.bak && docker rename <temp> <old> && docker rm -f <old>.bak.
    • Si falla: docker rm -f <temp> y notificar.
  4. Notificaciones
    Enviar un webhook, mensaje a Discord, Telegram o correo SMTP con el resultado de la operación. El mensaje debe incluir el ID de la imagen, el contenedor afectado y el motivo del rollback.

  5. Persistencia de estado
    Guardar la última ejecución y la versión anterior en un volumen montado para que el contenedor pueda recuperarse tras reinicios.

Cuándo aplicar esta solución

  • Entornos de producción o homelab con alta disponibilidad donde cualquier caída es inaceptable.
  • Servicios con healthchecks definidos (por ejemplo, aplicaciones web, bases de datos ligeras, proxies) que pueden validar su propio estado.
  • Stacks basados en Docker Compose que agrupan varios contenedores y requieren actualizaciones coordinadas.
  • Flujos CI/CD que no pueden esperar intervención manual para revertir una versión defectuosa.

No es adecuada cuando:

  • Los contenedores no disponen de HEALTHCHECK y no es posible añadirlo sin modificar la imagen.
  • El proceso de arranque es extremadamente largo (horas) y el timeout de 120 s resultaría insuficiente.
  • Se necesita integración con orquestadores como Kubernetes o Docker Swarm, donde existen mecanismos nativos de rollout y rollback.

Código

# docker-compose.yml minimal para el auto-updater
services:
  web:
    image: nginx:1.27
    labels:
      - "freshdock.enable=true"
      - "freshdock.policy=nightly"
  freshdock:
    image: ghcr.io/turbootzz/freshdock:latest
    command: ["run"]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    restart: unless-stopped
# Comando interno del actualizador (pseudocódigo)
docker pull "$IMAGE"
temp_id=$(docker create --label freshdock.temp=true "$IMAGE" "${OPTIONS[@]}")
docker start "$temp_id"

# Esperar healthcheck (max 120s)
for i in {1..12}; do
  status=$(docker inspect --format '{{.State.Health.Status}}' "$temp_id")
  if [[ "$status" == "healthy" ]]; then
    docker rename "$CONTAINER" "${CONTAINER}.old"
    docker rename "$temp_id" "$CONTAINER"
    docker rm -f "${CONTAINER}.old"
    notify_success "$CONTAINER" "$IMAGE"
    exit 0
  fi
  sleep 10
done

# Si llega aquí, el healthcheck falló
docker rm -f "$temp_id"
notify_failure "$CONTAINER" "$IMAGE"

Verificación

  1. Prueba de healthcheck

    • Modifica temporalmente el HEALTHCHECK de una imagen de prueba para que falle (exit 1).
    • Ejecuta el actualizador y confirma que el contenedor original sigue activo y que se envía la notificación de fallo.
  2. Rollback exitoso

    • Publica una nueva etiqueta que sí pasa el healthcheck.
    • Verifica que el contenedor se reemplaza y que el log muestra “update successful”.
  3. Persistencia de estado

    • Reinicia el contenedor freshdock.
    • Asegúrate de que la última ejecución y la versión anterior se conservan en el volumen y que el proceso continúa sin perder información.

Notas adicionales

  • Grace period sin healthcheck: si un contenedor carece de HEALTHCHECK, el actualizador puede usar un tiempo de gracia (p.ej., 10 s) para observar que el proceso no termina inmediatamente. No es tan fiable como un healthcheck, pero captura la mayoría de los fallos de arranque.
  • Redes container:X: al actualizar un contenedor que sirve como red compartida, el actualizador debe volver a conectar los contenedores dependientes después del swap. Esto se logra manteniendo los nombres de red y reusando la misma configuración de network_mode.
  • Migraciones: si el contenedor ejecuta una migración antes de iniciar, incluye un paso de “pre‑run” que lance el script de migración y aborta el rollout si el script devuelve error.
  • Compatibilidad: la herramienta funciona con Docker Engine 20+, Docker Desktop, Podman 4+ y con gestores de UI como Portainer o Dockge, siempre que tengan acceso al socket.
  • Seguridad: montar el socket en modo solo‑lectura minimiza el riesgo de escalada de privilegios. Asegúrate de que el contenedor freshdock no tenga capacidades adicionales.