Files
apps-registry/workloads/docs-portal/docs/playbooks/incidente-contencion-recursos-gitea-runner-2026-08.md
T
devopsandClaude Sonnet 5 b0b832f142
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 36s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 6m18s
docs(docs-portal): reflejar fixes de MTU, gitea-runner y cosign digest/rekor
Nuevo playbook del blackhole MTU cross-node (diagnostico + fix + validacion).
Actualiza el playbook de contencion de recursos Gitea/runner de 'pendiente'
a 'aplicado'. Actualiza cosign.md: firma por digest ya implementada, Rekor
como decision tomada (no TODO). Corrige 2 links que quedaron rotos al
renombrar la seccion de limitaciones (validado con mkdocs build --strict).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
2026-08-16 23:07:26 -05:00

6.3 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 aplicado (2026-08-16)

  1. scripts/deploy-lab.sh (mergeado a main): 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 recree el contenedor (la función es idempotente) — el runner queda con menos prioridad relativa que Gitea a propósito.
  2. docker-compose.yml de Gitea (fuera de este repo, en el host ZimaOS — /DATA/.casaos/apps/gitea/docker-compose.yml, acceso root-only): cpu_shares de gitea subido de 90 a 1024 (el default de Docker), aplicado en caliente con docker compose up -d y validado con docker inspect.
  3. Mover gitea-runner a k3d como job/pod separado: sigue quedando solo como recomendación documentada, no implementado — la contención real ya se resolvió con los límites de arriba, así que esto pasa a ser una mejora de aislamiento a futuro, no una urgencia.

Validación

Con el fix aplicado: docker inspect gitea --format '{{.HostConfig.CpuShares}}' confirma 1024, el contenedor se recreó limpio (Started, sin errores) y siguió sirviendo tráfico normal (200 OK en http://127.0.0.1:3002/ inmediatamente después). La correlación completa carga-pesada → reinicio se había visto en tres fechas distintas (2026-07-30, 2026-08-11 y el propio 2026-08-15 de este incidente) antes de este fix — no era un evento aislado. Queda por confirmar con el próximo pipeline pesado real que la contención no vuelva a aparecer, pero la causa raíz (pesos de CPU desbalanceados y sin techo de memoria en el runner) ya no está presente.