Volver al blog
InfraestructuraDevOpsDeuda técnica

Una instancia pequeña, cinco servicios y un OOM killer

Operar un SaaS con lo justo: producción, desarrollo, PostgreSQL, Redis y un validador de Odoo en la misma máquina. Qué se siente cuando el kernel elige a quién matar, por qué compilar en el servidor de producción es una mala idea que tomé a conciencia, y la distancia entre la infraestructura que sé montar y la que mi plataforma tiene hoy.

Oscar Alhdhair Vasquez Roncal 30 de julio de 2026 8 min de lectura

El inventario

MigrateFelix corre hoy sobre una sola instancia pequeña. En esa máquina conviven producción, el entorno de desarrollo, PostgreSQL, Redis y un validador de Odoo que instala módulos generados en una instancia real de Odoo 19 para comprobar que arrancan.

Ese último servicio es el que rompe el presupuesto de memoria. Un Odoo levantándose no es un proceso ligero, y no es una elección estética: la única forma de saber que un módulo generado funciona es instalarlo de verdad. Podría quitar el validador y la máquina respiraría. También dejaría de tener producto.

[CONFIRMAR: tipo de instancia exacto, memoria total y límites configurados por servicio — tomar de la configuración real antes de publicar, no aproximar]

Cómo se ve un OOM desde fuera

Lo que nadie te cuenta del out-of-memory killer de Linux es lo poco dramático que resulta. No hay un error. No hay una excepción. No hay un log de tu aplicación diciendo que algo salió mal, porque tu aplicación no llegó a enterarse: el kernel eligió un proceso, calculó cuál liberaba más memoria con menos coste, y lo terminó.

Desde fuera, lo que ves es que algo dejó de responder. Si tienes suerte, es un servicio con Restart= en su unidad de systemd y vuelve solo unos segundos después, y lo único que queda es un hueco en las métricas y unas cuantas peticiones fallidas que nadie asocia con nada. Si no tienes suerte, el que muere es PostgreSQL, y entonces mueren todos los demás detrás.

La consecuencia práctica es que tienes que ir a buscar la evidencia a los logs del kernel, porque tu stack de observabilidad de aplicación no la tiene. journalctl -k y buscar la línea de Out of memory: Killed process es la diferencia entre "se cayó otra vez, qué raro" y saber exactamente qué proceso murió, cuánta memoria estaba usando y qué más había corriendo en ese momento.

La segunda consecuencia es que el proceso que muere casi nunca es el culpable. El OOM killer no busca al que se pasó de la raya, busca al que libera más memoria. El validador de Odoo puede consumirse el margen entero y el que muere ser el servicio web de producción, simplemente porque era más grande en ese instante.

La decisión que más me incomoda: compilar en el servidor

El despliegue de este frontend funciona así. Un push a la rama principal dispara un pipeline que instala dependencias, comprueba tipos, pasa el linter, ejecuta los tests, corre la puerta de cobertura y hace un build. Todo eso en el runner de CI. Bien.

Y después se conecta por SSH al servidor y vuelve a compilar allí: git reset --hard origin/main, npm ci, npm run build, copiar los assets al directorio standalone, reiniciar el servicio.

Es una mala idea y sé por qué es una mala idea. Un build consume mucha memoria en la misma máquina donde vive producción, lo cual en una instancia con el margen justo significa que el propio despliegue es un candidato de primera para el OOM killer. Lo que se despliega no es el artefacto que CI validó, sino un artefacto nuevo compilado en otro sitio a partir del mismo commit — parecido, casi siempre idéntico, pero no el mismo. Y el servidor tiene que ser un árbol de trabajo de git con dependencias de desarrollo instaladas, lo que le da una superficie que una máquina de producción no debería tener.

Lo hago así porque construir el pipeline de artefactos —empaquetar, versionar, publicar, descargar en el destino— es trabajo real que todavía no he hecho, y porque de momento funciona. Eso es una respuesta honesta, no una buena respuesta.

El health check que decía que sí

Después de reiniciar, el despliegue espera unos segundos y hace un curl contra el puerto local. Si responde 200, declara el despliegue exitoso. Si no, revierte al commit anterior, reconstruye y reinicia.

El rollback automático está bien y me ha salvado. Pero el health check comprueba que el proceso responde, y eso no es lo mismo que comprobar que la aplicación funciona. Hubo un caso concreto: la copia de los assets estáticos al directorio standalone falló silenciosamente, la aplicación arrancó y devolvió 200 en la home, y el despliegue se declaró exitoso. Sin CSS y sin JavaScript. El curl estaba diciendo la verdad — el servidor respondía — pero la pregunta era la equivocada.

Ahora esas copias verifican que el directorio destino no quedó vacío y abortan si lo está. Es el mismo patrón del que ya escribí a propósito de los guardrails: un paso que se traga su propio fallo convierte un despliegue roto en un despliegue exitoso, y el problema no es el fallo, es la afirmación.

Sigue sin haber ventana de reinicio cero: entre que systemd para el servicio y lo vuelve a levantar hay unos segundos en los que el sitio no está. Con el tráfico actual es aceptable. No es un despliegue sin corte, y llamarlo así sería mentir.

La distancia entre lo que sé montar y lo que tengo

Esta es la parte del post que me interesa escribir, porque es la que normalmente no se escribe.

Sé montar la versión correcta de esto. He trabajado con Terraform organizado en módulos reutilizables y cuentas separadas para desarrollo, staging y producción. Con grupos de auto escalado, con VPCs con subredes públicas y privadas, peering y VPN site-to-site. Con pipelines que hacen análisis de seguridad y despliegues blue/green con rollback automático. Con métricas, alertas y trazabilidad de experimentos. Nada de lo que describo como "lo correcto" es teoría que haya leído.

Y mi propia plataforma no tiene casi nada de eso.

No hay infraestructura declarada como código: la máquina está configurada a mano, y si desaparece mañana, reconstruirla es un ejercicio de arqueología. No hay separación de entornos por cuenta ni por máquina — desarrollo y producción comparten kernel, memoria y disco, así que un experimento de desarrollo puede tumbar producción por agotamiento de memoria. No hay despliegue inmutable. No hay escalado horizontal, ni podría haberlo con estado local en la misma caja.

La tentación al escribir esto es justificarlo con una frase sobre priorizar producto sobre infraestructura, y hay algo de verdad ahí. Pero la razón más honesta es más simple: la infraestructura correcta es cara en tiempo y en dinero, y hasta ahora la incorrecta ha aguantado. Esa es una apuesta, la estoy haciendo con los ojos abiertos, y tiene fecha de vencimiento aunque yo no sepa cuál.

Lo que sí hago mientras tanto

Que la infraestructura sea mínima no obliga a que sea imprudente. Lo que sí sostengo:

  • Rollback automático en cada despliegue, con verificación después de revertir. Es lo primero que construiría de nuevo si perdiera todo lo demás.
  • Ningún paso del despliegue se traga su propio fallo. Las copias de assets verifican su resultado; el manejador de webhooks devuelve 5xx cuando falla en lugar de 200.
  • Presupuestar la memoria por servicio en lugar de dejar que se la repartan a codazos, para que quien se pase sea el candidato del OOM killer y no el vecino. [CONFIRMAR: qué límites hay realmente configurados hoy y en qué servicios — contrastar con la unidad de systemd de cada uno antes de publicar esta afirmación]
  • Revisar el entorno del proceso vivo, no los defaults del repositorio, cuando algo no cuadra.

Es una lista corta y deliberadamente aburrida. Cada punto existe porque su ausencia me costó algo.

Lo que me llevo

Operar con lo justo es una decisión legítima cuando sabes qué estás cambiando por qué. Deja de serlo en el momento en que empiezas a describir tu infraestructura por lo que sabes montar en vez de por lo que tienes puesto.

Si estás en la misma situación, el consejo concreto es este: escribe la lista de tu propia deuda de infraestructura, sin adornos, en un sitio donde tú la vuelvas a leer. No para arreglarla mañana — no vas a arreglarla mañana. Para que el día que decidas qué construir, la decisión la tome alguien que sabe dónde está parado.

¿Listo para migrar tu módulo?

MigrateFelix automatiza el trabajo repetitivo. Sube tu ZIP y obtén resultados en minutos.

Prueba MigrateFelix gratis →