# Incidente: contención de recursos entre Gitea y su runner (2026-08) | | | |---|---| | **Ventana del incidente** | 2026-08-15, durante el primer intento real de correr el pipeline de seguridad completo en `commerce-backend` | | **Servicio afectado** | `gitea` y `gitea-runner` (mismo host ZimaOS) | | **Impacto** | Un job de CI en curso (build pesado de Medusa + Trivy) fue cancelado a mitad de camino porque ambos contenedores se reiniciaron solos | ## Contexto Este hallazgo estaba **anotado como pendiente** en el playbook del [504 del túnel](incidente-504-gitea-tunnel.md): una ventana de latencia alta y algunos 502/530 habían coincidido, en su momento, con una ejecución pesada de Gitea Actions — anotado como "contención de recursos local, no relacionado al fix del túnel, vigilar si es recurrente". Al triplicar la carga agregando el mismo pipeline de seguridad a `commerce-backend` y `docs-portal`, se volvió a ver en vivo, esta vez con evidencia suficiente para confirmar la causa. ## Síntoma Un run de `build-medusa.yaml` (con Gitleaks ya en verde, build de Docker en curso) quedó cancelado a mitad de camino. El log del runner mostró, en la misma ventana de un par de minutos: ```text level=error msg="failed to fetch task" error="unavailable: 502 Bad Gateway" level=info msg="runner: ... shutdown initiated, waiting 0s for running jobs to complete before shutting down" level=warning msg="runner: ... cancelled in progress jobs during shutdown" level=info msg="Starting runner daemon" ``` El mismo patrón se repitió una segunda vez pocos minutos después. En paralelo, `docker ps` mostró que el contenedor `gitea` también se había reiniciado casi al mismo tiempo. ## Diagnóstico `docker inspect` sobre ambos contenedores en el momento del incidente mostró: - `ExitCode=0` y `RestartCount=0` en los dos — **no fue un crash ni un OOM-kill** (eso dejaría un `ExitCode` distinto de 0 o `OOMKilled=true`, y el conteo de reinicios de la política de Docker subiría). El `StartedAt` cambió igual, lo que indica un reinicio limpio disparado por algo externo al propio proceso — muy probablemente el supervisor de apps de CasaOS (`zimaos-app-management`), aunque no se pudo confirmar por logs (`/var/log/casaos/log.log` es de acceso root-only, sin sudo sin password disponible en esta sesión). - Memoria del host en el momento del incidente: **~1.3 GB libres de 15 GB totales**, con ~2 GB de swap en uso — el host estaba bajo presión real de memoria, no solo de CPU. - Ningún límite de recursos real en ninguno de los dos contenedores (ver detalle abajo). ## Causa raíz Ni `gitea` ni `gitea-runner` tienen aislamiento de recursos real frente al resto del host: | | `gitea` | `gitea-runner` | |---|---|---| | Límite de memoria | ~15.5 GB (≈ toda la RAM del host, no es un límite real) | **ninguno** | | Límite de CPU (cuota dura) | ninguno | ninguno | | Peso relativo de CPU (`cpu-shares`) | **90** (muy por debajo del default de Docker, 1024) | 0 → default Docker (**1024**) | Con `gitea` en 90 y `gitea-runner` en el default de 1024, bajo contención de CPU **el runner tiene ~11x más prioridad que el propio servidor Gitea** — lo opuesto a lo deseable, ya que Gitea es el servicio que necesita seguir respondiendo peticiones HTTP (incluidas las del propio runner haciendo polling) mientras un build pesado corre. Sumado a que `gitea-runner` no tiene techo de memoria: un job pesado (build de Docker de una imagen con `node_modules` grande + escaneo de Trivy con vuln + secret scanning) puede consumir memoria sin límite, empujando al host entero a swap y dejando lento/no-responsivo a todo lo demás — incluida la propia base de datos SQLite de Gitea (ver [incidente de SQLite en modo DELETE](incidente-sqlite-modo-delete-2026-08.md), que probablemente reaparezca con más frecuencia bajo este mismo tipo de presión, aunque WAL reduce el impacto). `gitea-runner` tampoco corre gestionado por el mismo `docker-compose.yml` de Gitea — se crea con un `docker run` suelto desde `scripts/deploy-lab.sh` (`create_gitea_runner_container()`), montando `/var/run/docker.sock` directo (sibling containers, ver [incidente de Docker-in-Docker](incidente-docker-in-docker-semgrep-2026-08.md)). Esto no es la causa del incidente de recursos, pero significa que cualquier ajuste de límites tiene que aplicarse en dos lugares distintos: el `docker-compose.yml` de `gitea` y el script que crea `gitea-runner`. !!! info "La concurrencia del runner ya estaba bien" `capacity: 1` en el `config.yaml` del runner ya limita la ejecución a **un solo job a la vez** — no es un problema de "demasiados jobs en paralelo". El problema es que incluso un solo job pesado, sin ningún límite de recursos, puede acaparar suficiente CPU/memoria como para dejar sin aire al resto del host. ## Fix propuesto (documentado, aplicación manual pendiente) 1. **`scripts/deploy-lab.sh`** (rama `fix/gitea-runner-resource-limits`, pendiente de mergear): agrega `--cpu-shares 512 --memory 8g --memory-swap 10g` a la creación de `gitea-runner`. Solo afecta a la próxima vez que se cree el contenedor (la función es idempotente), no al que ya está corriendo. 2. **Fix inmediato en caliente** (sin recrear contenedores), a aplicar manualmente por fuera de este repo: ```bash docker update --cpu-shares 1024 gitea docker update --cpu-shares 512 --memory 8g --memory-swap 10g gitea-runner ``` 3. **`docker-compose.yml` de Gitea** (fuera de este repo, acceso root-only): subir `cpu_shares` de `gitea` de 90 a 1024 — pendiente de aplicar manualmente, no se implementó todavía. 4. **Mover `gitea-runner` a k3d** como job/pod separado: evaluado como posible mejora de fondo (aislamiento real vía Kubernetes en vez de límites de Docker sueltos), pero queda **solo como recomendación documentada** — no implementado en esta vuelta. ## Validación Con `gitea` y `gitea-runner` estables (sin reinicios) después del incidente, se reintentó el mismo pipeline y corrió de punta a punta sin interrupciones — pero eso confirma que el host se estabilizó después del pico de carga, **no** que el fix de límites de recursos ya esté aplicado (sigue pendiente, ver arriba). La correlación completa carga-pesada → reinicio ya se había visto antes, en dos fechas distintas (2026-07-30 y 2026-08-11) además de esta — no es un evento aislado, es un patrón recurrente que ahora tiene una causa raíz concreta y una propuesta de fix.