tva
← Artículos

Más de cien contenedores Docker: nuestra rutina mensual de revisión de estado

Ejecutar más de cien contenedores Docker en producción no es inusual si se han incorporado servicios gradualmente durante varios años. Un stack de Supabase autoalojado suma por sí solo trece contenedores. Si se añaden un frontend web, varios servicios de API, workers en segundo plano, un stack de monitoreo y la agregación de registros, la cifra crece más rápido de lo previsto. Lo difícil no es desplegar estos contenedores, sino mantenerlos.

La mayoría de la documentación explica cómo poner en marcha los contenedores. Muy poca aborda qué ocurre seis meses después, cuando el disco se llena un domingo, cuando se descubre que un tercio de los contenedores no tiene política de reinicio o cuando un certificado SSL vence sin que nadie lo advierta porque se había silenciado la alerta de monitoreo. Este artículo documenta nuestra rutina mensual para un servidor de producción con más de cien contenedores.

Empezar por el disco

El disco es la causa más común de fallos graves en entornos Docker de larga duración. Docker acumula datos de formas que las herramientas habituales del sistema no muestran claramente. Ejecutar df -h muestra el uso del sistema de archivos, pero no indica que Docker mantiene cincuenta gigabytes de capas de contenedores detenidos, imágenes sin referencias y caché de compilación de seis meses de despliegues iterativos.

El punto de partida correcto es docker system df, que desglosa el uso de disco por imágenes, contenedores, volúmenes locales y caché de compilación. El resultado suele sorprender. Hemos visto servidores donde solo la caché de compilación superaba todas las capas de los contenedores en ejecución juntas, acumulada silenciosamente durante meses de compilaciones activadas por CI que nunca se limpiaron.

En la práctica, el dato más importante es la columna de espacio recuperable. Antes de tocar nada, establecemos una línea de base: cuánto espacio puede recuperarse y cómo evoluciona de un mes a otro. Si aumenta, la limpieza debe ser más exhaustiva o más frecuente.

Nuestra secuencia de limpieza sigue este orden. Primero, los volúmenes anónimos que ya no están referenciados por ningún contenedor. Desde Docker 23.0, docker volume prune deja intactos los volúmenes con nombre que no se utilizan, salvo que se añada --all; antes de añadir esa opción, compruebe que ningún volumen sin asociar contenga datos que todavía necesite:

docker volume prune -f

Después, las imágenes sin referencias: capas sin etiqueta que no utiliza ningún contenedor, ni en ejecución ni detenido:

docker image prune -f

Por último, si hemos confirmado que una reconstrucción completa es viable dentro de nuestro plazo de recuperación, todas las imágenes sin uso de más de siete días:

docker image prune -a --filter "until=168h" -f

La opción -a elimina todas las imágenes sin uso, no solo las que carecen de referencias. Solo la utilizamos después de verificar que todos los servicios pueden reconstruirse desde el registro dentro del tiempo de recuperación aceptable. Esa verificación se hace antes del comando, no después.

Políticas de reinicio

Las políticas de reinicio determinan qué ocurre cuando un contenedor termina inesperadamente o cuando el daemon de Docker vuelve a arrancar tras reiniciar el servidor. La mayoría de las guías de despliegue lo mencionan brevemente. En la práctica, una política incorrecta puede dejar servicios detenidos durante dos semanas sin que nadie lo advierta porque la alerta de monitoreo apuntaba al endpoint equivocado.

Docker ofrece cuatro políticas de reinicio. no es la predeterminada: el contenedor no se reinicia en ninguna circunstancia. always reinicia el contenedor siempre que se detiene, independientemente del código de salida; tras un docker stop manual, solo vuelve a arrancar cuando se reinicia el daemon o se inicia de nuevo el contenedor. unless-stopped se comporta como always, pero respeta las detenciones explícitas: si se ejecuta docker stop antes de reiniciar el servidor, el contenedor permanece detenido después. on-failure[:max-retries] solo reinicia cuando el código de salida es distinto de cero, con un límite opcional de reintentos antes de desistir.

Para servicios web sin estado y workers de API usamos unless-stopped. Si detenemos deliberadamente un contenedor durante una ventana de mantenimiento, debe permanecer detenido tras el siguiente reinicio en lugar de volver a arrancar inesperadamente. always lo volvería a iniciar en el siguiente reinicio del daemon, aunque lo hubiéramos detenido deliberadamente.

Para contenedores de migración de bases de datos o tareas de inicialización de una sola ejecución, la política correcta es no. Una migración fallida no debe entrar en un bucle. on-failure:3 resulta adecuada para contenedores que deben reintentar brevemente ante una dependencia temporalmente no disponible —por ejemplo, un consumidor de una cola externa que espera a que el broker sea accesible—, pero que no deben reintentarlo indefinidamente.

Nuestra comprobación mensual ejecuta un único comando sobre todos los contenedores:

docker inspect --format '{{.Name}} {{.HostConfig.RestartPolicy.Name}}' $(docker ps -aq)

Revisamos todo contenedor con la política no que no sea intencionadamente una tarea de ejecución única. En la mayoría de los casos, se trata de un servicio iniciado con docker run durante un incidente que nunca se incorporó formalmente a la configuración de Compose con una política de reinicio adecuada.

Rotación de registros

El controlador de registros predeterminado de Docker es json-file. Por defecto, no impone límites de tamaño. Un contenedor que emite un flujo moderado de líneas de registro puede generar cientos de gigabytes en varios meses. No es un riesgo teórico: es una de las causas más habituales de agotamiento del disco en servidores de producción configurados sin una gestión deliberada de los registros.

La solución es establecer una política global en /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "5"
  }
}

Esto limita los registros de cada contenedor a cinco archivos de cien megabytes cada uno: un máximo de quinientos megabytes por contenedor. Tras el cambio debe reiniciarse el daemon de Docker y, sobre todo, los contenedores deben recrearse, no simplemente reiniciarse, para que se apliquen los nuevos ajustes.

En realidad, la configuración de daemon.json solo se aplica a los contenedores creados después del cambio. Los existentes conservan indefinidamente su configuración original. Es el error más común que encontramos: se establece la política, se reinicia el daemon y se da por hecho que todos los contenedores ya la cumplen. No es así. Nuestra comprobación mensual verifica la configuración de registros de cada contenedor:

docker inspect --format '{{.Name}} {{.HostConfig.LogConfig}}' $(docker ps -q)

Los contenedores sin límites explícitos de tamaño se recrean con la configuración actualizada durante la siguiente ventana de mantenimiento. El orden importa: los servicios con estado deben conservar sus volúmenes de datos y los servicios dependientes deben arrancar en la secuencia correcta.

Vencimiento de certificados SSL

Los certificados SSL vencen. El monitoreo automatizado detecta la mayoría de los casos, pero también puede estar mal configurado, generar fatiga por alertas o fallar silenciosamente junto al servicio que debe vigilar. Nuestra rutina mensual incluye una comprobación manual independiente de cualquier sistema automatizado.

Para cada dominio accesible públicamente, comprobamos el certificado directamente:

echo | openssl s_client -connect domain.com:443 -servername domain.com 2>/dev/null   | openssl x509 -noout -enddate

El comando muestra la fecha notAfter. Todo certificado que venza en los próximos treinta días entra inmediatamente en la cola de renovación, independientemente de lo que diga el panel de monitoreo. La comprobación manual es el control de respaldo.

En la infraestructura de certificados que gestionamos nosotros mismos —utilizada por varios servicios internos— comprobamos los certificados intermedios por separado de los certificados finales. Un certificado intermedio vencido invalida toda la cadena aunque el certificado final siga vigente. Este fallo es menos evidente que el vencimiento de un certificado final: los navegadores y clientes pueden mostrar errores confusos en lugar del mensaje claro de «certificado vencido» que espera la mayoría de los ingenieros.

Mantenemos un script de shell que recorre una lista de dominios, extrae la fecha de vencimiento mediante openssl y muestra una advertencia si faltan menos de treinta días y una alerta crítica si faltan menos de siete. Se ejecuta como tarea cron, pero también lo ejecutamos manualmente durante la revisión mensual para confirmar que la salida de cron ha sido correcta. Las tareas cron fallan silenciosamente más a menudo de lo que suele creerse.

Límites de recursos de los contenedores

Sin límites de memoria, un contenedor que funciona mal puede agotar la RAM del servidor y activar el mecanismo OOM del kernel para terminar procesos ajenos. Sin límites de CPU, un proceso descontrolado puede privar de recursos a los contenedores vecinos durante suficiente tiempo como para provocar fallos en cadena. Ninguno de los dos casos es excepcional.

La revisión mensual comprueba los límites de recursos de todos los contenedores en ejecución:

docker stats --no-stream --format "table {{.Name}}	{{.CPUPerc}}	{{.MemUsage}}"

La columna de memoria muestra juntos el uso y el límite; un contenedor sin límite de memoria muestra la memoria total del servidor como límite, mientras que 0B / 0B solo aparece en contenedores que no están en ejecución. Revisamos cada uno y fijamos un límite adecuado. Para servicios HTTP sin estado, un límite de memoria de dos a cuatro veces el conjunto de trabajo observado es un punto de partida razonable. El objetivo no es lograr precisión absoluta, sino evitar que un crecimiento ilimitado derribe los servicios que comparten el servidor.

También revisamos el porcentaje de CPU durante la comprobación de estadísticas. Un contenedor que se mantiene cerca del cien por ciento de CPU (en docker stats, el 100 % equivale a un núcleo completo, por lo que son posibles valores superiores en un servidor multinúcleo) puede indicar un proceso descontrolado o recursos insuficientes para su carga de trabajo. Ambos casos merecen investigarse antes de la siguiente revisión mensual.

Comprobación de actualizaciones de imágenes

Las imágenes base reciben parches de seguridad con una frecuencia irregular. Un contenedor cuya imagen estaba actualizada hace seis meses puede estar usando una versión de nginx o PostgreSQL con vulnerabilidades conocidas. No descargamos y redesplegamos automáticamente todos los contenedores cada mes: eso crea más riesgo del que elimina. Pero sí comparamos lo que está en ejecución con las versiones actuales.

El enfoque práctico es verificar mensualmente la versión fijada de cada servicio frente al registro de cambios del proyecto de origen. Para servicios con una etiqueta flotante como latest o 16-alpine, descargamos la imagen y comparamos su digest para determinar si ha cambiado. Si cambió, revisamos las diferencias antes de desplegar.

Sin embargo, la disciplina más importante es abandonar las etiquetas flotantes. Un servicio redesplegado silenciosamente con un cambio incompatible porque su etiqueta latest apuntaba a una nueva versión principal es más difícil de diagnosticar que uno que usa una imagen antigua conocida. Fije las versiones y actualícelas deliberadamente.

La disciplina

En conjunto, la revisión mensual cubre seis áreas: uso y limpieza de disco, políticas de reinicio, rotación de registros, vencimiento de certificados SSL, límites de recursos y actualización de imágenes. En un servidor bien documentado, estas tareas no requieren más de noventa minutos en total. Su valor está en realizarlas con una periodicidad fija, antes de que algo falle, más que en las comprobaciones individuales.

Los sistemas de producción se deterioran gradualmente. Se acumulan datos en el disco. Los registros crecen. Los certificados se acercan a su vencimiento. Las imágenes quedan desactualizadas. Ninguno de estos procesos genera una alerta hasta que se supera un umbral. La revisión mensual transforma el mantenimiento reactivo en previsible, lo que cambia por completo la forma de operar.


Artículos relacionados

Artículos relacionados