Intenté reducir nuestro stack de doce contenedores a cuatro. Dos de mis tres conclusiones eran falsas.
SOIT Team

Los proyectos autoalojados pierden a la mayoría de sus futuros usuarios en el primer comando del README, y el nuestro nombra doce servicios en una sola línea. La incidencia #25 hacía la pregunta evidente: solo quiero echarle un vistazo, ¿de verdad los necesito todos? La respuesta es no. Con siete de los doce basta y, como tres de esos siete son tareas de una sola vez que terminan solas, únicamente cuatro contenedores quedan residentes: postgres, minio, api y web. Lo más útil del ejercicio, sin embargo, es que el stack recortado se ejecutó de verdad en lugar de razonarse sobre el papel. Dos de las tres conclusiones sacadas de leer el código resultaron falsas, y las dos falsas eran justo las que habrían dejado a un recién llegado mirando una URL muerta.
La forma más rápida de saber si una dependencia es real no es la guía de despliegue sino el health check, porque una guía documenta intenciones y un health check documenta comportamiento. En el endpoint de readiness los tres backends reciben trato distinto: si la base de datos no responde se lanza un 503, si el almacenamiento de objetos no responde se lanza un 503, y si el almacén vectorial no responde el campo pasa a leerse como no disponible y el endpoint sigue devolviendo 200. Esa asimetría nombra los requisitos duros sin rodeos: un Postgres accesible y una raíz de almacenamiento donde se pueda escribir. Todo lo demás es negociable en principio, y el resto de este recorrido trata de qué negociaciones sobreviven al contacto con la imagen publicada.
Raíz de almacenamiento no es lo mismo que MinIO, y el adaptador resuelve efectivamente a un URI de fichero local cuando no se configura otra cosa, así que la primera conclusión fue que el almacén de objetos podía irse. No puede. Apuntar el almacenamiento a un directorio local levantó la API devolviendo que el almacenamiento de objetos no está disponible, y construir el adaptador a mano dentro del contenedor dio el error real: permiso denegado sobre una ruta que nadie había pedido. La causa es un ayudante de normalización que recorta las barras de ambos extremos de la ruta raíz, convirtiendo una ruta absoluta en relativa; esa ruta relativa se resuelve luego contra el directorio de trabajo de la imagen, que el Dockerfile copia como root mientras el proceso corre sin privilegios. Montar un volumen tampoco lo salva, porque Docker crea el punto de montaje también como root. Así que MinIO se queda, cosa barata con un par de cientos de megabytes, y el fallo de fondo está registrado como incidencia #43.
El grupo vectorial se va en bloque, y conviene ser preciso sobre lo que eso cuesta. Milvus depende de etcd para los metadatos y de MinIO para los datos, así que esos nombres viajan juntos. La API arranca igualmente sin ellos porque la conexión vectorial se establece de forma perezosa en el primer uso y no durante la inyección de dependencias. Lo que no hace es degradarse a resultados vacíos: la implementación vectorial en memoria está reservada a las pruebas y, a diferencia del puerto de secretos, nunca consulta la variable de entorno. El resultado es que la plataforma arranca, las funciones no vectoriales van bien, readiness informa honestamente del almacén vectorial como no disponible sin dejar de declararse lista, y la recuperación de conocimiento lanza un error en cuanto la usas. Esa conclusión sí se sostuvo.
La segunda conclusión equivocada es la que ninguna lectura habría producido. Esa respuesta honesta de readiness tardó treinta y cuatro segundos en volver, porque la sonda vectorial tiene que resolver un nombre de host que ya no existe y nada acota cuánto puede esperar. La sonda se escribió para fallar de forma blanda, no para fallar rápido. Mientras tanto, Compose comprueba la salud de la API con una petición de tres segundos y un margen de cinco, de modo que la comprobación no puede pasar y el contenedor queda marcado permanentemente como no saludable aunque el servicio detrás funcione y acepte inicios de sesión. El contenedor web declara que depende de que la API esté saludable, así que en un arranque normal la interfaz no llega a levantarse. El arreglo es una sola bandera, arrancar web omitiendo dependencias, y el timeout ausente en la sonda está registrado como incidencia #44.
Tres servicios son realmente opcionales, cada uno por un motivo distinto. Vault puede quedarse apagado: dejar vacíos su URL y su token cambia el almacén por uno de secretos en proceso, cosa que el cableado permite en entornos de desarrollo, al precio que anuncia su propio nombre, que los secretos no sobreviven a un reinicio. Redis no es una pregunta sino tres, y sus respuestas difieren: el bus de eventos puede volver a su implementación en memoria, que solo entrega dentro de un único proceso; la caché de permisos degrada con elegancia porque un Redis inalcanzable se lee como fallo de caché y quien llama vuelve a consultar la base de datos; y el limitador de tasa no tiene equivalente en memoria, pero solo se invoca cuando hay configurado un límite por herramienta o una cuota diaria. Prescindir de Redis en una demo es seguro siempre que no configures esas dos cosas, el único punto de esta lista que depende de lo que pienses demostrar.
Un servicio no se elimina sino que se pliega. El dispatcher de outbox ejecuta el outbox transaccional, y su ajuste controla dónde ocurre ese trabajo, no si ocurre: compose desactiva la ruta en proceso y corre la misma lógica en un contenedor dedicado, y una sola variable de entorno lo invierte para una demo. Producción hace lo contrario y lo impone en el código en vez de aconsejarlo en la documentación, porque el despacho y la atención de peticiones en un mismo proceso compiten por los mismos recursos y un reinicio interrumpe ambos a la vez. El worker de ingesta de conocimiento, por último, tiene un solo cometido y ninguna razón de existir si tu demo nunca sube un documento; y ya que estamos, las pesadas dependencias de aprendizaje automático que muchos suponen dentro de esa imagen viven en realidad en otro grupo opcional.
Nada de esto es una forma de despliegue soportada, y conviene decir por qué el valor por defecto pide doce. Cada servicio recortado aquí corresponde a algo que producción exige al arrancar: la validación en tiempo de ejecución se niega a iniciar sin el bus de eventos en Redis, sin el proceso dispatcher dedicado, sin un gestor de secretos real, sin telemetría y sin verificación de firmas de plugins. La topología por defecto apunta a la forma de producción, no a la de demostración, y la distancia entre ambas acaba siendo medible en un puñado de variables de entorno, que resulta ser el camino más rápido para entender la arquitectura. Quédate también con la otra lección: leer el código te da hipótesis, ejecutarlo te da conclusiones. El repositorio está en github.com/soit-ai/soit, el quickstart completo en docs/quickstart.md, y si te atascas en algún paso de la versión recortada, el gestor de incidencias es el sitio para decirlo.