Gameboy-Gate — Aufnahmeprüfung für jedes Projekt

Gate de validation obligatoire pour tout projet déployé sur le serveur Gameboy. Garantit que les limites de ressources, les variables d'environnement, le rate-limiting et la résilience au déploiement sont correctement configurés avant toute mise en production.

Qu'est-ce que Gameboy-Gate — Aufnahmeprüfung für jedes Projekt ?

Gameboy-Gate est un skill de validation (gate) obligatoire pour tout projet déployé sur le serveur Gameboy (un Hetzner CAX11 ARM avec 4 Go de RAM). Il automatise la vérification de l'état du serveur, le calcul des limites de mémoire/CPU en fonction du type de projet (Node.js, Python, Go, Redis, etc.), la persistance de ces limites dans la base de données Coolify pour éviter les régressions lors des déploiements, la configuration des variables d'environnement (NODE_OPTIONS, UV_THREADPOOL_SIZE), l'activation du rate-limiting Traefik, le réglage des limites de descripteurs de fichiers Docker, et un test de charge avec 50 utilisateurs concurrents pendant 30 secondes. Le résultat final est PASS (release autorisé) ou FAIL (release bloqué) selon des critères stricts : p99 < 3000 ms, < 0,1 % de requêtes échouées, mémoire stable sous 90 % du conteneur. Le skill est conçu pour être exécuté par l'assistant Claude Code sur macOS et Windows, avec les valeurs réelles des adresses IP et des URLs stockées dans un fichier local gitignoré (gameboy-gate/gameboy.local.md). Il s'intègre parfaitement à l'infrastructure Coolify du serveur Gameboy et garantit que chaque déploiement ne dépasse pas les ressources disponibles, évitant ainsi les OOM et les pannes de service.

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add Cem-Tas96/claude-skills

Installed? Explore more Productivité et collaboration skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

🎮 Gameboy-Gate — Aufnahmeprüfung für jedes Projekt

PFLICHT: Jedes Projekt das auf dem Gameboy (<GAMEBOY_IP>) deployed wird, muss diesen Gate bestehen — egal wer Claude nutzt, egal welches Projekt. Kein Release ohne PASS.

🔒 Verbindungsdaten lokal: Reale Werte für <GAMEBOY_IP>, <GAMEBOY_SSH>, <COOLIFY_URL>, <WATCHDOG_*> stehen in der gitignored Datei gameboy-gate/gameboy.local.md (NICHT im public Repo). Vor jedem Gate-Lauf diese Datei lesen und die Platzhalter ersetzen.

KONTEXT: Der Gameboy

Server: Hetzner CAX11 (ARM)
IP:     <GAMEBOY_IP>          # real: siehe gameboy.local.md
SSH:    <GAMEBOY_SSH>         # real: siehe gameboy.local.md
RAM:    4GB total
CPU:    2x vCPU (ARM Neoverse-N1 @ 2.0GHz)
Disk:   40GB SSD
OS:     Ubuntu (Docker + Coolify)
Coolify UI: <COOLIFY_URL>     # real: siehe gameboy.local.md

Feste Overhead-Reservierung (NIE unterschreiten):

OS + Kernel:              400MB
Coolify (inkl. DB, Redis, Proxy, Sentinel): 900MB
Safety-Buffer:            200MB
──────────────────────────────
FREE für Projekte:       ~2500MB  (aus 4096MB Gesamt)

Projekte die bereits laufen (Stand 26.06.2026 — reale Namen siehe gameboy.local.md):

  • app-workflow (Activepieces): 1536MB limit / 768MB reservation
  • app-billing (Next.js): 512MB limit / 256MB reservation
  • app-landing (Static): 256MB limit / 128MB reservation
  • Belegt: 2304MB limit / 1152MB reservation

GATE-ABLAUF (in dieser Reihenfolge, kein Überspringen)

Phase G0: Server-Snapshot holen

Immer zuerst — das ist die Wahrheit über den aktuellen Server-Zustand:

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

Daraus ableiten:

  • TOTAL_RAM_MB (fast immer 3840MB = 3.7GB als Bytes)
  • COOLIFY_OVERHEAD_MB = Summe aller Coolify-eigenen Container (coolify, coolify-db, coolify-redis, coolify-sentinel, coolify-proxy) aktuell
  • RUNNING_PROJECTS_MB = Summe der mem_limit aller laufenden Projekt-Container
  • AVAILABLE_MB = TOTAL_RAM_MB - 400 (OS) - COOLIFY_OVERHEAD_MB - RUNNING_PROJECTS_MB - 200 (safety)

Phase G1: Limit-Berechnung (KI-gesteuert, keine Hardcodes)

Basierend auf dem Projekt-Typ berechnet die KI die richtigen Limits:

PROJEKT-TYP ERKENNEN:
- Node.js / Next.js App       → BASE_MB = 350, WORKER_OVERHEAD = 50 pro Worker
- Python / Django / FastAPI   → BASE_MB = 200, WORKER_OVERHEAD = 30 pro Worker
- Go / Rust Service           → BASE_MB = 80,  WORKER_OVERHEAD = 20 pro Worker
- Static-Site (nginx/caddy)   → BASE_MB = 64,  WORKER_OVERHEAD = 0
- Postgres                    → BASE_MB = 128, WORKER_OVERHEAD = 10 pro Connection
- Redis                       → BASE_MB = 64,  WORKER_OVERHEAD = 0
- Background-Worker (queues)  → BASE_MB = 200, WORKER_OVERHEAD = 50 pro Worker
- AI/LLM-Service (lokal)      → BASE_MB = 800+ (mindestens 2GB wenn Modell lokal)

Limit-Formeln:

MEM_LIMIT_MB    = min(AVAILABLE_MB * 0.8, BASE_MB * 3)   -- 80% des Verfügbaren, max 3x Base
MEM_RESERVATION = MEM_LIMIT_MB * 0.5                      -- 50% als garantiertes Minimum
MEM_SWAP_MB     = MEM_LIMIT_MB * 1.4                      -- 40% Swap-Overhead erlaubt
CPU_LIMIT       = round(2.0 * (MEM_LIMIT_MB / AVAILABLE_MB), 1)  -- proportional zu RAM-Anteil

NODE_OPTIONS_MB = floor(MEM_LIMIT_MB * 0.70)              -- V8 Heap = 70% des Container-Limits
UV_THREADPOOL_SIZE = min(8, cpu_count * 2)                -- libuv Threads

Validation-Checks VOR dem Setzen:

  • MEM_LIMIT_MB >= 128 (Minimum um zu starten)
  • AVAILABLE_MB - MEM_LIMIT_MB >= 200 (Safety-Buffer bleibt übrig)
  • Summe aller mem_limits ≤ 3500MB (kein Overcommit über 85% physischer RAM)
  • Wenn Validation fehlschlägt → BLOCKIERT, Cem informieren

Phase G2: Limits permanent in Coolify DB setzen

Das ist der kritische Fix gegen deploy-Regressionen. Coolify regeneriert docker-compose.yaml bei jedem Deploy aus seiner eigenen DB. Nur wenn die Limits in der DB stehen, überleben sie Deploys.

Wenn die App-UUID bekannt ist:

<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>';
\"
"

UUID aus Coolify-URL holen: <COOLIFY_URL>/project/.../application/<UUID>/...

Wenn UUID unbekannt → via DB suchen:

docker exec coolify-db psql -U coolify -d coolify -c "SELECT uuid, name FROM applications ORDER BY created_at DESC LIMIT 10;"

Phase G3: Environment-Variablen setzen

In .env des Projekts (via echo >> .env oder Coolify UI):

# For Node.js/Next.js:
NODE_OPTIONS=--max-old-space-size=<NODE_OPTIONS_MB>
UV_THREADPOOL_SIZE=<UV_THREADPOOL_SIZE>

# For workflow apps (Activepieces etc.):
AP_MAX_CONCURRENT_JOBS=5     # prevents sandbox explosion
AP_TRIGGER_DEFAULT_POLL_INTERVAL=15  # reduces DB baseline load

# For all apps:
# (no DEBUG logging in production!)
NODE_ENV=production

WICHTIG: Kein Zeilenumbruch-Bug! Jede Variable auf eigener Zeile prüfen:

grep -c '^[A-Z_].*=' .env  # sollte gleich sein wie die Anzahl Variablen
# Keine Zeile darf zwei '=' enthalten außer in Werten
awk -F= 'NF>2 && $1 ~ /^[A-Z_]/' .env && echo "BUG: doppelte = in Zeile!"

Phase G4: Traefik Rate-Limiting aktivieren

Für JEDES neue öffentliche Service:

<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 'Rate-limiting config written'
"

Traefik picked das automatisch auf (watch=true) — kein Restart nötig.

Middleware dann zum App-Router hinzufügen (in der App-spezifischen Traefik-Konfig):

middlewares:
  - rate-limit-strict

Phase G5: File Descriptor Limits setzen

Docker daemon muss Limits kennen. Einmalig pro Gameboy-Setup, bleibt persistent:

<GAMEBOY_SSH> "
# Prüfen ob already set
grep -q 'nofile' /etc/docker/daemon.json && echo 'already set' || {
  # Backup und Update
  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 updated')
  \"
  # ACHTUNG: systemctl restart docker startet alle Container neu!
  # Nur wenn kein aktiver Traffic → mit Cem abstimmen
  echo 'NEEDS: systemctl restart docker (kurzer Downtime!)'
}
"

Phase G6: Load-Test (die eigentliche Aufnahmeprüfung)

50 concurrent users, 60 Sekunden:

# Test 1: Basis-Erreichbarkeit
curl -o /dev/null -s -w "HTTP=%{http_code} %{time_total}s\n" https://<APP_URL>/

# Test 2: 50 concurrent HTTP-Requests (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%"

# Test 3: Memory während Last monitoren
<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

Phase G7: PASS/FAIL Kriterien

PASS (alle müssen erfüllt sein):

KriteriumGrenzwert
HTTP Status2xx oder 3xx (kein 502/503/504)
p99 Latenz< 3000ms
Memory-Wachstumstabil (< +20% zwischen t=0 und t=60s)
Memory bleibt unter Limit< 90% des Container-Limits
Keine Failed-Requests< 0.1%
Server nach Test erreichbarHTTP 200/302 auf allen URLs
CPU nach Test< 80% sustained

FAIL (einer reicht für BLOCKIERT):

  • Irgendein Container ist nach dem Test down/restarted
  • Memory hat Limit erreicht (> 95%)
  • Failed requests > 1%
  • p99 > 5000ms
  • Watchdog hat während des Tests eingegriffen (prüfen: <WATCHDOG_LOG>)

Phase G8: Gate-Ergebnis

═══════════════════════════════════════════════════════
🎮 GAMEBOY-GATE ERGEBNIS
═══════════════════════════════════════════════════════
Projekt:     [Name]
App-UUID:    [Coolify UUID]
Test-Zeit:   [Datum/Uhrzeit]

LIMIT-BERECHNUNG:
  Server-RAM:              3840MB
  Coolify-Overhead:        [n]MB (gemessen)
  Andere Projekte:         [n]MB (Summe Limits)
  Verfügbar:               [n]MB
  Zugewiesen:              MEM=[n]m, CPU=[n], NODE_OPTIONS=[n]MB

CHECKS:
  ✅/❌ Coolify DB Limits persistent gesetzt
  ✅/❌ NODE_OPTIONS korrekt (kein Newline-Bug)
  ✅/❌ Rate-Limiting aktiv
  ✅/❌ File Descriptor Limit 65536
  ✅/❌ Redis maxmemory policy gesetzt (wenn Redis)
  ✅/❌ Load-Test 50c/30s bestanden

LOAD-TEST:
  Req/s:    [n]     (PASS wenn > 10)
  p50:      [n]ms
  p99:      [n]ms   (PASS wenn < 3000ms)
  Failed:   [n]%    (PASS wenn < 0.1%)
  Mem-Peak: [n]MB   (PASS wenn < 90% des Limits)

═══════════════════════════════════════════════════════
URTEIL: ✅ PASS — RELEASE ERLAUBT
       ❌ FAIL — RELEASE BLOCKIERT ([Grund])
═══════════════════════════════════════════════════════

HÄUFIGE FAIL-GRÜNDE UND FIXES

FAIL: NODE_OPTIONS nicht gesetzt

# Prüfen:
docker exec <container> printenv NODE_OPTIONS
# Fix:
grep -n 'NODE_OPTIONS' .env  # auf Newline-Bug prüfen
# Bug: HOST=0.0.0.0NODE_OPTIONS=... → Fix:
sed -i 's/HOST=0.0.0.0NODE_OPTIONS=/HOST=0.0.0.0\nNODE_OPTIONS=/' .env

FAIL: Container OOM nach Test

Root-Cause fast immer eines von:

  1. NODE_OPTIONS nicht gesetzt → V8 nimmt Container-RAM komplett
  2. Sandbox-Prozesse (AP, Code-Execution) ohne Concurrency-Limit
  3. Memory-Leak im App-Code (seltener)
# Prüfen ob Restart stattfand:
docker inspect <container> --format '{{.RestartCount}}'
# Prüfen ob OOM-Kill:
dmesg | grep -i 'oom\|killed' | tail -10

FAIL: p99 > 3000ms

  1. Heavy API-Endpoint ohne Caching → Redis-Cache implementieren
  2. PG-Connection-Pool erschöpft → MAX_CONCURRENT_REQUESTS oder Pool-Size erhöhen
  3. Forward-Auth-Overhead pro Request → JWT-Caching einbauen

FAIL: Failed Requests > 0.1%

  1. Traefik-Timeout kürzer als App-Response → Traefik-Timeouts erhöhen
  2. FD-Limit erschöpft → Schritt G5 (ulimits)
  3. PG max_connections erschöpft → max_connections=200 in PG setzen

DEPLOY-RESILIENZ: Das Deploy-Überlebens-Protokoll

Jeder Coolify-Deploy muss folgendes garantieren damit der Server nicht abstürzt:

Vor jedem Deploy prüfen:

<GAMEBOY_SSH> "
# Free RAM must be > 800MB
FREE=\$(free -m | awk '/^Mem:/{print \$7}')
echo "Available: \${FREE}MB"
[ \$FREE -lt 800 ] && echo 'WARNUNG: Zu wenig RAM für Deploy!' || echo 'RAM OK für Deploy'
# Disk must be > 5GB free (Image-Pull needs space)
DISK=\$(df / | awk 'NR==2{print \$4}')
echo "Disk free: \$((DISK/1024))MB"
[ \$DISK -lt 5242880 ] && echo 'WARNUNG: Zu wenig Disk!' || echo 'Disk OK'
"

Was Coolify bei Deploy macht (und was wir sicherstellen müssen):

  1. Coolify liest seine DB → generiert docker-compose.yaml mit den in DB gesetzten Limits (G2 → persistent ✅)
  2. Container stoppt → kurzer Downtime
  3. Neuer Container startet → Healthcheck wartet bis App bereit
  4. Traefik bekommt neuen Backend-Endpunkt

Healthcheck-Konfiguration (in Coolify UI oder direkt in DB):

docker exec coolify-db psql -U coolify -d coolify -c "
UPDATE applications SET
  health_check_enabled = true,
  health_check_path = '/api/v1/flags',  -- or '/', '/health', '/ping'
  health_check_port = '80',
  health_check_interval = 15,
  health_check_timeout = 5,
  health_check_retries = 3,
  health_check_start_period = 90  -- 90s startup time for heavy apps
WHERE uuid = '<APP_UUID>';
"

MEMORY-BUDGET-TABELLE (Stand 26.06.2026)

GAMEBOY 4GB RAM — BUDGET-ÜBERSICHT
════════════════════════════════════════════════════════════
System/OS:              400MB (fix)
Coolify-Stack:          ~950MB (coolify+db+redis+sentinel+proxy)
────────────────────────────────────────────────────────────
VERFÜGBAR FÜR PROJEKTE: ~2650MB
════════════════════════════════════════════════════════════
app-workflow:           1536MB limit / 768MB reservation
app-billing:             512MB limit / 256MB reservation
app-landing:             256MB limit / 128MB reservation
────────────────────────────────────────────────────────────
NOCH FREI:              ~346MB limit-Spielraum
════════════════════════════════════════════════════════════

WARNUNG: Der Gameboy ist zu ~91% seiner Limit-Kapazität belegt.
Neues Projekt > 300MB braucht Rebalancing (existierendes 
Projekt verkleinern oder Coolify-Stack optimieren).

Wenn ein neues Projekt mehr als 200MB braucht → erst prüfen ob landing (256MB) auf 128MB reduziert werden kann (statische Site, braucht wenig RAM).


CHECKLISTE FÜR JEDEN DEPLOY (Quick Reference)

VOR DEPLOY:
  [ ] Freier RAM > 800MB
  [ ] Disk > 5GB frei
  [ ] Coolify DB Limits gesetzt (nicht 0!)
  [ ] NODE_OPTIONS in .env korrekt (keine Zeilenumbruch-Bugs)
  [ ] Rate-Limiting für öffentliche Endpoints konfiguriert

NACH DEPLOY:
  [ ] Container läuft (docker ps → Up X seconds (healthy))
  [ ] URL erreichbar (HTTP 2xx/3xx)
  [ ] NODE_OPTIONS aktiv (docker exec <c> printenv NODE_OPTIONS)
  [ ] Memory stabil (docker stats → nicht an Limit)
  [ ] Kein OOM-Kill (dmesg | grep -i 'oom\|killed' | tail -10 ist leer)

TROUBLESHOOTING: Bekannte Probleme

db.deploy.deployed wiederholt sich

# Wenn ein Healthcheck fehlschlägt, wiederholt Coolify endlos:
# Prüfen:
<GAMEBOY_SSH> 'docker inspect <container> | jq ".State.Health"'
# Fix: Gesundheitspfad korrigieren (G3) oder Startzeit erhöhen

Coolify zeigt falsche/missing Limits

# Direkt die DB fragen:
<GAMEBOY_SSH> "docker exec coolify-db psql -U coolify -d coolify -c \"SELECT uuid, name, limits_memory, limits_memory_swap, limits_memory_reservation, limits_cpus FROM applications WHERE uuid='<APP_UUID>';\""
# Wenn NULL → Schritt G2 erneut ausführen

Traefik gibt 502 nach Deploy

# Prüfen: Container läuft?
docker ps | grep <app>
# Prüfen: Port Mapping korrekt?
docker port <container>
# Prüfen: Traefik Service Discovery
docker logs coolify-proxy --tail 20 2>&1 | grep <app>
# Fix: Meist hilft ein einfaches 'docker restart <container>'

Skills associés