🎮 Gameboy-Gate — Prueba de admisión para cada proyecto
OBLIGATORIO: Todo proyecto desplegado en el Gameboy (
<GAMEBOY_IP>) debe pasar este Gate — sin importar quién use Claude, ni qué proyecto sea. Sin lanzamiento sin PASS.
🔒 Datos de conexión local: Los valores reales para
<GAMEBOY_IP>,<GAMEBOY_SSH>,<COOLIFY_URL>,<WATCHDOG_*>se encuentran en el archivo ignorado por gitgameboy-gate/gameboy.local.md(NO en el repositorio público). Antes de cada ejecución del Gate, leer este archivo y reemplazar los marcadores de posición.
CONTEXTO: El Gameboy
Servidor: Hetzner CAX11 (ARM)
IP: <GAMEBOY_IP> # real: ver gameboy.local.md
SSH: <GAMEBOY_SSH> # real: ver gameboy.local.md
RAM: 4GB total
CPU: 2x vCPU (ARM Neoverse-N1 @ 2.0GHz)
Disco: 40GB SSD
SO: Ubuntu (Docker + Coolify)
UI Coolify: <COOLIFY_URL> # real: ver gameboy.local.md
Reserva fija de overhead (NUNCA reducir por debajo de esto):
SO + Kernel: 400MB
Coolify (incl. DB, Redis, Proxy, Sentinel): 900MB
Buffer de seguridad: 200MB
──────────────────────────────
LIBRE para proyectos: ~2500MB (de 4096MB totales)
Proyectos ya en ejecución (al 26/06/2026 — nombres reales en gameboy.local.md):
app-workflow(Activepieces): límite 1536MB / reserva 768MBapp-billing(Next.js): límite 512MB / reserva 256MBapp-landing(Estático): límite 256MB / reserva 128MB- Ocupado: 2304MB límite / 1152MB reserva
FLUJO DEL GATE (en este orden, sin saltos)
Fase G0: Obtener snapshot del servidor
Siempre primero — esta es la verdad sobre el estado actual del servidor:
<GAMEBOY_SSH> "
free -h &&
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}' &&
df -h / &&
docker inspect \$(docker ps -q) --format '{{.Name}}: mem={{.HostConfig.Memory}}' 2>/dev/null | sort
"
De esto derivar:
TOTAL_RAM_MB(casi siempre 3840MB = 3.7GB en bytes)COOLIFY_OVERHEAD_MB= suma de todos los contenedores propios de Coolify (coolify, coolify-db, coolify-redis, coolify-sentinel, coolify-proxy) actualRUNNING_PROJECTS_MB= suma de mem_limit de todos los contenedores de proyectos en ejecuciónAVAILABLE_MB= TOTAL_RAM_MB - 400 (SO) - COOLIFY_OVERHEAD_MB - RUNNING_PROJECTS_MB - 200 (seguridad)
Fase G1: Cálculo de límites (guiado por IA, sin valores fijos)
Basado en el tipo de proyecto, la IA calcula los límites correctos:
DETECTAR TIPO DE PROYECTO:
- App Node.js / Next.js → BASE_MB = 350, WORKER_OVERHEAD = 50 por worker
- Python / Django / FastAPI → BASE_MB = 200, WORKER_OVERHEAD = 30 por worker
- Servicio Go / Rust → BASE_MB = 80, WORKER_OVERHEAD = 20 por worker
- Sitio estático (nginx/caddy) → BASE_MB = 64, WORKER_OVERHEAD = 0
- Postgres → BASE_MB = 128, WORKER_OVERHEAD = 10 por conexión
- Redis → BASE_MB = 64, WORKER_OVERHEAD = 0
- Worker en segundo plano (colas) → BASE_MB = 200, WORKER_OVERHEAD = 50 por worker
- Servicio AI/LLM (local) → BASE_MB = 800+ (al menos 2GB si el modelo es local)
Fórmulas de límite:
MEM_LIMIT_MB = min(AVAILABLE_MB * 0.8, BASE_MB * 3) -- 80% de lo disponible, máximo 3x Base
MEM_RESERVATION = MEM_LIMIT_MB * 0.5 -- 50% como mínimo garantizado
MEM_SWAP_MB = MEM_LIMIT_MB * 1.4 -- 40% de overhead de swap permitido
CPU_LIMIT = round(2.0 * (MEM_LIMIT_MB / AVAILABLE_MB), 1) -- proporcional a la porción de RAM
NODE_OPTIONS_MB = floor(MEM_LIMIT_MB * 0.70) -- V8 Heap = 70% del límite del contenedor
UV_THREADPOOL_SIZE = min(8, cpu_count * 2) -- Hilos libuv
Validaciones ANTES de establecer:
-
MEM_LIMIT_MB >= 128(mínimo para iniciar) -
AVAILABLE_MB - MEM_LIMIT_MB >= 200(el buffer de seguridad permanece) - Suma de todos los mem_limits ≤ 3500MB (sin sobrecompromiso superior al 85% de la RAM física)
- Si la validación falla → BLOQUEADO, informar a Cem
Fase G2: Establecer límites de forma permanente en la base de datos de Coolify
Esta es la solución crítica contra las regresiones de despliegue. Coolify regenera docker-compose.yaml en cada despliegue desde su propia base de datos. Solo si los límites están en la base de datos sobrevivirán a los despliegues.
Si se conoce el UUID de la aplicación:
<GAMEBOY_SSH> "
docker exec coolify-db psql -U coolify -d coolify -c \"
UPDATE applications SET
limits_memory = '<MEM_LIMIT_MB>m',
limits_memory_swap = '<MEM_SWAP_MB>m',
limits_memory_swappiness = 10,
limits_memory_reservation = '<MEM_RESERVATION>m',
limits_cpus = '<CPU_LIMIT>',
limits_cpu_shares = 1024
WHERE uuid = '<APP_UUID>';
SELECT uuid, name, limits_memory, limits_cpus FROM applications WHERE uuid='<APP_UUID>';
\"
"
Obtener UUID de la URL de Coolify: <COOLIFY_URL>/project/.../application/<UUID>/...
Si el UUID es desconocido → buscar en la base de datos:
docker exec coolify-db psql -U coolify -d coolify -c "SELECT uuid, name FROM applications ORDER BY created_at DESC LIMIT 10;"
Fase G3: Establecer variables de entorno
En .env del proyecto (mediante echo >> .env o la interfaz de Coolify):
# Para Node.js/Next.js:
NODE_OPTIONS=--max-old-space-size=<NODE_OPTIONS_MB>
UV_THREADPOOL_SIZE=<UV_THREADPOOL_SIZE>
# Para aplicaciones de flujo de trabajo (Activepieces, etc.):
AP_MAX_CONCURRENT_JOBS=5 # evita la explosión de sandboxes
AP_TRIGGER_DEFAULT_POLL_INTERVAL=15 # reduce la carga basal de la base de datos
# Para todas las aplicaciones:
# (¡sin registro DEBUG en producción!)
NODE_ENV=production
IMPORTANTE: ¡Sin errores de salto de línea! Verificar cada variable en su propia línea:
grep -c '^[A-Z_].*=' .env # debería ser igual al número de variables
# Ninguna línea debe contener dos '=' excepto en valores
awk -F= 'NF>2 && $1 ~ /^[A-Z_]/' .env && echo "ERROR: ¡doble = en línea!"
Fase G4: Activar limitación de tasa en Traefik
Para CADA nuevo servicio público:
<GAMEBOY_SSH> "
cat > /data/coolify/proxy/dynamic/rate-limit-global.yaml << 'EOF'
http:
middlewares:
rate-limit-strict:
rateLimit:
average: 60
burst: 120
period: 1s
sourceCriterion:
ipStrategy:
depth: 1
rate-limit-api:
rateLimit:
average: 30
burst: 60
period: 1s
sourceCriterion:
ipStrategy:
depth: 1
EOF
echo 'Configuración de limitación de tasa escrita'
"
Traefik lo recoge automáticamente (watch=true) — no se necesita reinicio.
Luego agregar el middleware al enrutador de la aplicación (en la configuración de Traefik específica de la aplicación):
middlewares:
- rate-limit-strict
Fase G5: Establecer límites de descriptores de archivo
El daemon de Docker debe conocer los límites. Una vez por configuración del Gameboy, permanece persistente:
<GAMEBOY_SSH> "
# Verificar si ya está establecido
grep -q 'nofile' /etc/docker/daemon.json && echo 'ya establecido' || {
# Copia de seguridad y actualización
cp /etc/docker/daemon.json /etc/docker/daemon.json.bak.\$(date +%Y%m%d)
python3 -c \"
import json
with open('/etc/docker/daemon.json') as f: d = json.load(f)
d['default-ulimits'] = {'nofile': {'Name': 'nofile', 'Hard': 65536, 'Soft': 65536}}
with open('/etc/docker/daemon.json', 'w') as f: json.dump(d, f, indent=2)
print('daemon.json actualizado')
\"
# ATENCIÓN: systemctl restart docker reinicia todos los contenedores.
# Solo si no hay tráfico activo → coordinar con Cem
echo 'NECESITA: systemctl restart docker (¡pequeño tiempo de inactividad!)'
}
"
Fase G6: Prueba de carga (la verdadera prueba de admisión)
50 usuarios concurrentes, 60 segundos:
# Prueba 1: Accesibilidad básica
curl -o /dev/null -s -w "HTTP=%{http_code} %{time_total}s\n" https://<APP_URL>/
# Prueba 2: 50 solicitudes HTTP concurrentes (30s)
/usr/sbin/ab -n 3000 -c 50 -t 30 -r -k https://<APP_URL>/ 2>&1 | grep -E "Requests|Failed|Time per|50%|90%|99%"
# Prueba 3: Monitorear memoria durante la carga
<GAMEBOY_SSH> "for i in \$(seq 1 6); do sleep 10; docker stats --no-stream --format '{{.Name}}: {{.MemUsage}} {{.MemPerc}} CPU={{.CPUPerc}}' | grep -v NAMES; echo '---'; done" &
MONITOR_PID=\$!
/usr/sbin/ab -n 5000 -c 50 -t 55 -r https://<APP_URL>/ 2>/dev/null
wait \$MONITOR_PID
Fase G7: Criterios de APROBACIÓN/RECHAZO
APROBADO (todos deben cumplirse):
| Criterio | Límite |
|---|---|
| Estado HTTP | 2xx o 3xx (sin 502/503/504) |
| Latencia p99 | < 3000ms |
| Crecimiento de memoria | estable (< +20% entre t=0 y t=60s) |
| Memoria permanece por debajo del límite | < 90% del límite del contenedor |
| Sin solicitudes fallidas | < 0.1% |
| Servidor accesible después de la prueba | HTTP 200/302 en todas las URLs |
| CPU después de la prueba | < 80% sostenido |
RECHAZADO (uno es suficiente para BLOQUEAR):
- Algún contenedor está caído/reiniciado después de la prueba
- La memoria alcanzó el límite (> 95%)
Solicitudes fallidas > 1%p99 > 5000ms- El watchdog intervino durante la prueba (verificar:
<WATCHDOG_LOG>)
Fase G8: Resultado del Gate
═══════════════════════════════════════════════════════
🎮 RESULTADO DEL GATE DEL GAMEBOY
═══════════════════════════════════════════════════════
Proyecto: [Nombre]
App-UUID: [UUID de Coolify]
Hora de prueba: [Fecha/Hora]
CÁLCULO DE LÍMITES:
RAM del servidor: 3840MB
Overhead de Coolify: [n]MB (medido)
Otros proyectos: [n]MB (suma de límites)
Disponible: [n]MB
Asignado: MEM=[n]m, CPU=[n], NODE_OPTIONS=[n]MB
VERIFICACIONES:
✅/❌ Límites de BD Coolify persistentes
✅/❌ NODE_OPTIONS correcto (sin error de salto de línea)
✅/❌ Limitación de tasa activa
✅/❌ Límite de descriptor de archivo 65536
✅/❌ Política maxmemory de Redis establecida (si es Redis)
✅/❌ Prueba de carga 50c/30s superada
PRUEBA DE CARGA:
Req/s: [n] (APROBADO si > 10)
p50: [n]ms
p99: [n]ms (APROBADO si < 3000ms)
Fallos: [n]% (APROBADO si < 0.1%)
Pico Mem: [n]MB (APROBADO si < 90% del límite)
═══════════════════════════════════════════════════════
VEREDICTO: ✅ APROBADO — LANZAMIENTO PERMITIDO
❌ RECHAZADO — LANZAMIENTO BLOQUEADO ([Motivo])
═══════════════════════════════════════════════════════
CAUSAS FRECUENTES DE RECHAZO Y SOLUCIONES
RECHAZO: NODE_OPTIONS no establecido
# Verificar:
docker exec <contenedor> printenv NODE_OPTIONS
# Solución:
grep -n 'NODE_OPTIONS' .env # verificar error de salto de línea
# Error: HOST=0.0.0.0NODE_OPTIONS=... → Solución:
sed -i 's/HOST=0.0.0.0NODE_OPTIONS=/HOST=0.0.0.0\nNODE_OPTIONS=/' .env
RECHAZO: Contenedor OOM después de la prueba
La causa raíz es casi siempre una de:
NODE_OPTIONSno establecido → V8 toma toda la RAM del contenedor- Procesos sandbox (AP, ejecución de código) sin límite de concurrencia
- Fuga de memoria en el código de la aplicación (menos común)
# Verificar si hubo reinicio:
docker inspect <contenedor> --format '{{.RestartCount}}'
# Verificar si hubo OOM-Kill:
dmesg | grep -i 'oom\|killed' | tail -10
RECHAZO: p99 > 3000ms
- Endpoint API pesado sin caché → implementar caché Redis
- Pool de conexiones PG agotado → aumentar
MAX_CONCURRENT_REQUESTSo tamaño del pool - Sobrecarga de Forward-Auth por solicitud → implementar caché JWT
RECHAZO: Solicitudes fallidas > 0.1%
- Timeout de Traefik más corto que la respuesta de la aplicación → aumentar timeouts de Traefik
- Límite FD agotado → Paso G5 (ulimits)
- max_connections de PG agotado → establecer
max_connections=200en PG
RESILIENCIA DE DESPLIEGUE: El protocolo de supervivencia del despliegue
Cada despliegue de Coolify debe garantizar lo siguiente para que el servidor no se bloquee:
Antes de cada despliegue verificar:
<GAMEBOY_SSH> "
# La RAM libre debe ser > 800MB
FREE=\$(free -m | awk '/^Mem:/{print \$7}')
echo "Disponible: \${FREE}MB"
[ \$FREE -lt 800 ] && echo 'ADVERTENCIA: ¡Poca RAM para el despliegue!' || echo 'RAM OK para el despliegue'
# El disco debe tener > 5GB libres (la descarga de imagen necesita espacio)
DISK=\$(df / | awk 'NR==2{print \$4}')
echo "Disco libre: \$((DISK/1024))MB"
[ \$DISK -lt 5242880 ] && echo 'ADVERTENCIA: ¡Poco disco!' || echo 'Disco OK'
"
Lo que hace Coolify durante el despliegue (y lo que debemos asegurar):
- Coolify lee su base de datos → genera docker-compose.yaml con los límites establecidos en la base de datos (G2 → persistente ✅)
- El contenedor se detiene → breve tiempo de inactividad
- El nuevo contenedor se inicia → Healthcheck espera hasta que la aplicación esté lista
- Traefik obtiene un nuevo endpoint de backend
Configuración de Healthcheck (en la interfaz de Coolify o directamente en la base de datos):
docker exec coolify-db psql -U coolify -d coolify -c "
UPDATE applications SET
health_check_enabled = true,
health_check_path = '/api/v1/flags', -- o '/', '/health', '/ping'
health_check_port = '80',
health_check_interval = 15,
health_check_timeout = 5,
health_check_retries = 3,
health_check_start_period = 90 -- 90s de tiempo de inicio para aplicaciones pesadas
WHERE uuid = '<APP_UUID>';
"
TABLA DE PRESUPUESTO DE MEMORIA (al 26/06/2026)
GAMEBOY 4GB RAM — RESUMEN DEL PRESUPUESTO
════════════════════════════════════════════════════════════
Sistema/SO: 400MB (fijo)
Stack Coolify: ~950MB (coolify+db+redis+sentinel+proxy)
────────────────────────────────────────────────────────────
DISPONIBLE PARA PROYECTOS: ~2650MB
════════════════════════════════════════════════════════════
app-workflow: límite 1536MB / reserva 768MB
app-billing: límite 512MB / reserva 256MB
app-landing: límite 256MB / reserva 128MB
────────────────────────────────────────────────────────────
AÚN LIBRE: ~346MB de margen de límite
════════════════════════════════════════════════════════════
ADVERTENCIA: El Gameboy está a ~91% de su capacidad de límite.
Un proyecto nuevo > 300MB necesita reequilibrio (reducir
un proyecto existente u optimizar el stack de Coolify).
Si un proyecto nuevo necesita más de 200MB → primero verificar si landing (256MB) puede reducirse a 128MB (sitio estático, necesita poca RAM).
LISTA DE VERIFICACIÓN PARA CADA DESPLIEGUE (Referencia rápida)
ANTES DEL DESPLIEGUE:
[ ] RAM libre > 800MB
[ ] Disco > 5GB libre
[ ] Límites de BD Coolify establecidos (¡no 0!)
[ ] NODE_OPTIONS en .env correcto (sin errores de salto de línea)
[ ] Limitación de tasa configurada para endpoints públicos
DESPUÉS DEL DESPLIEGUE:
[ ] Contenedor en ejecución (docker ps → Up X segundos (healthy))
[ ] URL accesible (HTTP 2xx/3xx)
[ ] NODE_OPTIONS activo (docker exec <c> printenv NODE_OPTIONS)
[ ] Memoria estable (docker stats → no al límite)
[ ] Sin OOM-Kill (dmesg | grep -i 'oom')