Cuando empecé a desarrollar GalopApp (una plataforma analítica para carreras de caballos con IA), caí en la trampa habitual del ingeniero de software moderno: intentar usar la arquitectura «perfecta» desde el día cero, además de que quería probar unos cuantos temas de GCP. Pero…
Desplegué el proyecto en Google Cloud Platform (GCP). Tenía el backend troceado en Cloud Functions, la base de datos en Cloud SQL, los archivos estáticos en Google Cloud Storage (GCS) y la autenticación delegada a Firebase. Sobre el papel, era un sistema infinitamente escalable, tolerante a fallos y «moderno».
En la práctica, para un pet project (y para muchas startups en fase temprana), era un dolor de cabeza:
- Tiempos de arranque en frío (cold starts): La primera petición a la API tardaba segundos en responder porque GCP tenía que levantar la instancia de la función.
- Complejidad de despliegue: Orquestar actualizaciones entre distintas funciones sin romper el contrato de la API era lento y frágil.
- Costes impredecibles y altos: Especialmente la instancia de base de datos relacional y el trasiego de datos entre servicios. Y para un petproject, que sé que no voy a monetizar nunca, más de 150€/mes para mi, era demasiado.
- Overhead mental: Ejecutar procesos pesados de scraping o de entrenamiento de Machine Learning requería montar colas de mensajes (Pub/Sub) y funciones con tiempos de espera largos.
Me di cuenta de que mi problema no era la escala de usuarios, sino la complejidad operativa. Así que tomé una decisión pragmática: mandar el modelo Serverless a paseo y volver a lo básico. Un servidor, un monolito (además que siempre he sido partidario del «Monolith first» 😉 )
El Stack Actual: El «Monolito Modular» en VPS
Hoy, GalopApp vive feliz en un modesto pero robusto servidor VPS Hetzner (un CX32) bajo una arquitectura que yo llamo el Monolito Modular. Todo corre orquestado por Docker Compose.
La topología de red es estricta y segura por diseño (superficie de exposición mínima). Solamente un contenedor tiene permisos para hablar con el mundo exterior abriendo los puertos 80 y 443; el resto se comunican en una red bridge privada interna de Docker.
1. El Portero: Caddy
El único servicio expuesto a internet es Caddy, actuando como Reverse Proxy. Su magia reside en que se encarga del enrutamiento y, sobre todo, de renovar automáticamente los certificados TLS (HTTPS) con Let’s Encrypt sin que yo tenga que configurar ningún cron ni certbot. Caddy sirve la SPA de React (los ficheros estáticos) en la ruta raíz, y redirige limpiamente el tráfico /api/* al backend interno.
2. El Core: FastAPI (Python)
El corazón del sistema es un contenedor de FastAPI. En lugar de tener microservicios repartidos, tengo routers modulares dentro del mismo proyecto (/races, /horses, /admin). Como la instancia está siempre encendida, la latencia es de milisegundos. Además, esto me permite lanzar tareas pesadas (como scrapear resultados al final de una jornada o regenerar predicciones de IA) usando simples Background Tasks in-process, sin complicarme con message brokers externos.
3. Persistencia Relacional: MySQL 8.0
Un contenedor persistente de MySQL guarda todo el histórico de carreras, pedigríes y estadísticas. Toda la lógica pesada se delega a sentencias SQL crudas o agregaciones, evitando cargar pandas o hacer transformaciones lentas en la capa de la aplicación. No me gusta tener las BD relacionales en contenedores (me parece un antipatrón feo, soy viejo) pero hoy, con los storages que hay, ya no es tanto problema.
4. Almacenamiento S3: MinIO
Para guardar archivos (como los PDFs generados dinámicamente) y los modelos pre-entrenados de LightGBM, necesitaba algo parecido a Google Cloud Storage o AWS S3. La solución drop-in fue MinIO. Levantar un contenedor de MinIO me dio una API 100% compatible con S3 sin salir de mi propio servidor.
5. Identidad centralizada: Keycloak
Para no gestionar contraseñas a mano, levanté un contenedor de Keycloak. El frontend autentica contra Keycloak, que devuelve un JWT firmado (RS256). El backend de FastAPI simplemente se descarga las claves públicas (JWKS) de Keycloak y valida los tokens en cada request. Lo tengo configurado para que Keycloak sea la fuente de identidad, pero la base de datos (MySQL) mantiene los roles (admin o user), así que sigo teniendo control total sobre los permisos a nivel de SQL.
Un CI/CD aburridamente simple y efectivo
El despliegue ya no requiere orquestar Terraform ni pelear con permisos IAM. Mi pipeline en GitHub Actions hace lo siguiente cuando hago push a la rama main:
- Conecta por SSH al VPS.
- Hace
git pull. - Reconstruye la imagen del backend (
docker compose build backend). - Ejecuta las migraciones de base de datos (
alembic upgrade head) en un contenedor efímero. - Actualiza los servicios en vivo con
docker compose up -d.
Todo sucede en apenas un par de minutos, y los health-checks nativos de Docker garantizan que el contenedor nuevo no acepta tráfico si falla al arrancar.
Conclusión
Migrar a un VPS y apostar por un Monolito Modular con Docker Compose ha sido la mejor decisión técnica para este proyecto. He reducido drásticamente el overhead mental de despliegue, he aniquilado los tiempos de arranque en frío, mis costes son fijos (y bajos, menos de 10€/mes), y el sistema sigue teniendo componentes aislados y de primer nivel (Keycloak, MinIO, MySQL).
A veces, la arquitectura perfecta no es la más sofisticada, sino la que te permite iterar y enviar código a producción con la menor fricción posible. Y para el 99% de los proyectos, un buen servidor y un monolito bien estructurado es todo lo que necesitas. Lo primordial es tener el código bien estructurado y separado, para que las decisiones de despliegue sean triviales (pasar de monolito a n despliegues o a cloud functions)