Problema

En entornos de producción con contenedores Docker, los fallos inesperados (por ejemplo, procesos que se detienen, puertos que dejan de responder o configuraciones que quedan corruptas) pueden pasar desapercibidos hasta que un cliente experimenta un error. Cuando el orquestador no está configurado para reiniciar automáticamente (por política, por falta de restart: always o porque el contenedor forma parte de un sandbox de pruebas), el estado fallido persiste y el servicio queda inoperativo.

Este patrón se repite en muchos setups: un contenedor crítico se detiene, el equipo de operaciones recibe una alerta tardía y debe intervenir manualmente para volver a ponerlo en marcha, validar que el servicio está saludable y registrar la acción. La intervención manual introduce tiempo de inactividad, errores humanos y una carga operativa que escala con el número de microservicios.

Causa

Los orígenes de estos fallos pueden agruparse en tres categorías habituales:

  1. Fallos internos del proceso – errores de aplicación, excepciones no capturadas o consumo excesivo de recursos que hacen que el proceso principal termine. Docker solo marca el contenedor como “exited”, pero no lo reinicia si la política lo impide.
  2. Problemas de infraestructura – pérdida de conectividad de red, discos saturados o reinicios del host que dejan contenedores en estado “dead”. En estos casos Docker tampoco interviene automáticamente.
  3. Acciones de prueba o caos – inyección deliberada de fallos (por ejemplo, con herramientas como chaos-mesh o scripts personalizados) para validar la resiliencia. Si el entorno de pruebas no permite reinicios automáticos, el contenedor queda detenido a la espera de una reparación manual.

En los tres casos la raíz del problema es la falta de un mecanismo que detecte el estado no deseado, ejecute una acción pre‑aprobada y verifique que el contenedor volvió a estar operativo antes de considerar el incidente resuelto.

Solución

Un agente de auto‑curación para Docker aborda el ciclo completo: monitorización, decisión, ejecución y auditoría. La arquitectura típica consta de los siguientes componentes:

  1. Watcher – proceso ligero que consulta el API de Docker (docker ps -a --filter "status=exited" o la API HTTP) a intervalos definidos. Su única responsabilidad es identificar contenedores que no estén en el estado deseado.
  2. Policy Engine – tabla de correspondencia entre estado detectado y acción permitida. Cada regla incluye:
    • Identificador del contenedor (nombre, etiqueta o patrón de imagen).
    • Acción permitida (restart, run script, scale up).
    • Condiciones de verificación (puertos abiertos, endpoint /healthz, logs recientes).
  3. Executor – módulo que, bajo control estricto, ejecuta la acción definida. No permite shells arbitrarios; solo llama a la API de Docker o a scripts pre‑aprobados almacenados en un directorio de solo‑lectura.
  4. Verifier – después de la acción, vuelve a inspeccionar el contenedor y realiza pruebas de salud (por ejemplo, curl -f http://localhost:80/health). Si la verificación falla, el agente registra el intento y aborta, evitando bucles infinitos.
  5. Logger/Auditor – escribe cada evento en un log estructurado (JSON) o en un sistema de observabilidad (ELK, Loki). Los logs incluyen timestamp, contenedor, acción, resultado y duración.

Implementación práctica

A continuación se muestra un esquema de cómo montar este agente con Bash + Docker CLI y un pequeño daemon en Python (solo como ejemplo; cualquier lenguaje que pueda consumir la API de Docker sirve).

  1. Definir la política en un archivo YAML (policy.yaml):
containers:
  - name: sandbox-demo-api
    action: restart
    health_check:
      url: http://localhost:80/health
      timeout: 5
  - name: another-service
    action: run_script
    script: /opt/recovery/fix_another.sh
    health_check:
      url: http://localhost:8080/status
      timeout: 10
  1. Watcher en Bash (ejecutado cada 30 s mediante systemd o cron):
#!/usr/bin/env bash
docker ps -a --filter "status=exited" --format "{{.Names}}" | while read cname; do
  if grep -q "$cname" /opt/selfheal/active_containers.txt; then
    # Ya está bajo proceso de reparación, saltar
    continue
  fi
  echo "$cname" >> /opt/selfheal/active_containers.txt
  /opt/selfheal/engine.py "$cname" &
done
  1. Engine en Python (simplificado):
#!/usr/bin/env python3
import sys, yaml, subprocess, time, requests, json, os

def load_policy():
    with open('/opt/selfheal/policy.yaml') as f:
        return yaml.safe_load(f)

def find_rule(name, policy):
    for c in policy['containers']:
        if c['name'] == name:
            return c
    return None

def restart_container(name):
    subprocess.check_call(['docker', 'start', name])

def run_script(path):
    subprocess.check_call([path])

def verify(url, timeout):
    try:
        r = requests.get(url, timeout=timeout)
        return r.status_code == 200
    except Exception:
        return False

def main():
    cname = sys.argv[1]
    policy = load_policy()
    rule = find_rule(cname, policy)
    if not rule:
        print(f'No policy for {cname}', file=sys.stderr)
        return
    start = time.time()
    if rule['action'] == 'restart':
        restart_container(cname)
    elif rule['action'] == 'run_script':
        run_script(rule['script'])
    else:
        print('Unsupported action', file=sys.stderr)
        return
    ok = verify(rule['health_check']['url'], rule['health_check']['timeout'])
    duration = time.time() - start
    log = {
        "container": cname,
        "action": rule['action'],
        "verified": ok,
        "duration_s": round(duration, 2),
        "timestamp": time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
    }
    print(json.dumps(log))
    # Cleanup
    os.remove('/opt/selfheal/active_containers.txt')
if __name__ == '__main__':
    main()

Este ejemplo muestra la lógica esencial: detección, acción permitida, verificación y registro estructurado. En producción se recomienda:

  • Ejecutar el engine dentro de un contenedor con permisos limitados (--cap-drop ALL y solo docker.sock montado).
  • Usar un sistema de colas (Redis, RabbitMQ) para evitar concurrencia.
  • Añadir reintentos con back‑off exponencial y límite de intentos.

Cuándo aplicar esta solución

Señales de que vale la pena:

  • Contenedores críticos que no usan políticas de reinicio automático por razones de seguridad o pruebas.
  • Entornos donde la tolerancia a fallos es alta y el tiempo de inactividad debe mantenerse en segundos.
  • Equipos que ya cuentan con un pipeline de CI/CD y pueden versionar políticas de reparación como código.

Escenarios donde no es necesario:

  • Clústeres Kubernetes con livenessProbe y restartPolicy configurados; la propia plataforma ya gestiona la auto‑curación.
  • Servicios sin requisitos de alta disponibilidad (por ejemplo, jobs batch que pueden fallar y volver a lanzar).
  • Infraestructuras donde la política de “no reinicio automático” es deliberada y la intervención humana es parte del proceso de auditoría.

Código

# Instalar dependencias (Docker CLI, python3, pip, requests, pyyaml)
apt-get update && apt-get install -y docker.io python3-pip
pip3 install pyyaml requests

# Crear directorios
mkdir -p /opt/selfheal/{active_containers.txt,policy.yaml}

# Guardar los scripts mostrados arriba en /opt/selfheal/
chmod +x /opt/selfheal/engine.py /opt/selfheal/watcher.sh

# Configurar systemd service para el watcher
cat > /etc/systemd/system/selfheal-watcher.service <<'EOF'
[Unit]
Description=Docker Self-Healing Watcher
After=network.target docker.service

[Service]
ExecStart=/opt/selfheal/watcher.sh
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now selfheal-watcher.service

Verificación

  1. Inyección de fallo – detén manualmente un contenedor incluido en la política: docker stop sandbox-demo-api.
  2. Observa el log – el engine imprimirá un JSON con verified: true y la duración. En un entorno con Loki, busca container="sandbox-demo-api" y verifica que la marca de tiempo coincida con la reparación.
  3. Prueba de salud – abre el endpoint definido (curl -f http://localhost:80/health). Debería responder 200 después de la reparación.
  4. Escenarios de error – modifica la política para que la URL de health check sea incorrecta y confirma que el agente registra verified: false y no entra en bucle infinito.

Notas adicionales

  • Seguridad: montar solo docker.sock sin acceso a /var/run/docker.sock de forma global reduce la superficie de ataque. Usa usuarios sin privilegios de root dentro del contenedor del agente.
  • Escalabilidad: si gestionas cientos de contenedores, agrupa la monitorización por etiquetas (docker ps -a --filter "label=autoheal"). El motor de políticas puede cargar reglas dinámicamente desde un repositorio Git.
  • Persistencia de estado: guarda los IDs de contenedores en proceso de reparación en una base de datos ligera (SQLite) para evitar duplicados tras reinicios del agente.
  • Integración con alertas: envía eventos a Slack, PagerDuty o Prometheus Alertmanager para que el equipo tenga visibilidad incluso cuando la reparación sea exitosa.

Con este enfoque, la auto‑curación pasa de ser una prueba de concepto aislada a una práctica reutilizable que se adapta a cualquier stack Docker que requiera disponibilidad continua sin depender de orquestadores más complejos.