Files
apps-registry/workloads/docs-portal/docs/playbooks/incidente-contencion-recursos-gitea-runner-2026-08.md
T
devops be3e5c2a4c docs(devsecops): documentar 4+1 hallazgos de infraestructura y TODOs de Cosign
5 playbooks nuevos en docs/playbooks/ del portal:
- SQLite en modo DELETE causando SQLITE_BUSY, por que WAL lo resuelve.
- Anchors YAML no soportados por el parser de Gitea Actions, incluido
  el diagnostico equivocado inicial (indentacion) que se corrigio
  despues -- documentado con honestidad como leccion de proceso.
- Docker-in-Docker vs sibling containers, y por que Semgrep corre
  nativo en un venv en vez de como container aparte.
- El SNI/realm HTTP del tunel rompiendo especificamente a Cosign
  (docker push funcionaba, cosign sign no), conectado con el
  incidente ya documentado del 504 -- misma capa de ingress, dos
  fallas distintas.
- Contencion de recursos entre Gitea y su runner: causa raiz real
  confirmada en vivo durante esta misma sesion (ambos contenedores
  reiniciados a mitad de un build, sin limites de CPU/memoria reales),
  con fix propuesto (ver rama fix/gitea-runner-resource-limits en
  scripts/) pendiente de aplicacion manual.

Se actualiza el playbook del 504 para linkear hacia el nuevo hallazgo
de contencion, cerrando el loop que habia quedado abierto ahi.

cosign.md: nueva seccion "Limitaciones conocidas y mejoras futuras"
con los dos TODO reales -- firma por tag en vez de digest (con el
warning real de Cosign visto en los logs) y transparency log de Rekor
deshabilitado, explicado en simple.

index.md: tabla resumen de los 3 pipelines (imagen + adaptacion de
Semgrep por stack), diagrama generalizado para reflejar que es el
mismo patron en los 3 workflows, y "que falta" actualizado.

Validado con mkdocs build --strict (0 errores, 0 warnings) antes de
commitear.
2026-08-15 15:17:43 -05:00

6.4 KiB

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: 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:

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, 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). 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:

    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.