Community생산성 & 협업github.com

Gameboy-Gate — 프로젝트 출시 심사

개인 Claude Code 스킬로, 모든 머신(macOS + Windows) 간에 동기화되며 ~/.claude/skills/에 위치합니다.

Gameboy-Gate — 프로젝트 출시 심사란 무엇인가요?

Gameboy-Gate는 Claude Code를 사용하는 모든 프로젝트가 Gameboy 서버(Hetzner CAX11, 4GB RAM)에 배포되기 전에 통과해야 하는 필수적인 자동화된 승인 심사입니다. 이 스킬은 서버 스냅샷 수집, AI 기반 리소스 한도 계산, Coolify 데이터베이스에 영구적인 한도 설정, 환경 변수 구성, Rate-Limiting 활성화, File Descriptor 한도 설정, 부하 테스트 실행, 그리고 명확한 PASS/FAIL 기준 제시를 포함한 8단계 게이트 프로세스를 자동화합니다. 주요 가치는 배포 후에도 리소스 한도가 유지되도록 보장하여 Coolify가 배포 시 docker-compose.yaml을 재생성할 때 발생하는 회귀 문제를 방지하는 데 있습니다. 이 스킬은 macOS와 Windows 모두에서 Claude Code와 함께 작동하도록 설계되었습니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add Cem-Tas96/claude-skills

Installed? Explore more 생산성 & 협업 skills: steipete/gemini, steipete/gh-issues, steipete/skill-creator · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

🎮 Gameboy-Gate — 프로젝트 출시 심사

필수: Gameboy(<GAMEBOY_IP>)에 배포되는 모든 프로젝트는 이 게이트를 통과해야 합니다 — Claude를 사용하는 사람이나 프로젝트 종류에 관계없이. PASS 없이는 출시 불가.

🔒 로컬 연결 정보: <GAMEBOY_IP>, <GAMEBOY_SSH>, <COOLIFY_URL>, <WATCHDOG_*> 의 실제 값은 gitignored 파일인 gameboy-gate/gameboy.local.md에 있습니다 (퍼블릭 Repo에 없음). 각 게이트 실행 전에 이 파일을 읽고 플레이스홀더를 교체하세요.

맥락: Gameboy

서버: Hetzner CAX11 (ARM)
IP:     <GAMEBOY_IP>          # 실제: gameboy.local.md 참고
SSH:    <GAMEBOY_SSH>         # 실제: gameboy.local.md 참고
RAM:    4GB 전체
CPU:    2x vCPU (ARM Neoverse-N1 @ 2.0GHz)
디스크: 40GB SSD
OS:     Ubuntu (Docker + Coolify)
Coolify UI: <COOLIFY_URL>     # 실제: gameboy.local.md 참고

고정 오버헤드 예약 (절대 미만으로 설정 금지):

OS + 커널:              400MB
Coolify (DB, Redis, Proxy, Sentinel 포함): 900MB
안전 버퍼:              200MB
──────────────────────────────
프로젝트에 사용 가능: ~2500MB  (총 4096MB 중)

현재 실행 중인 프로젝트 (2026년 6월 26일 기준 — 실제 이름은 gameboy.local.md 참고):

  • app-workflow (Activepieces): 1536MB 한도 / 768MB 예약
  • app-billing (Next.js): 512MB 한도 / 256MB 예약
  • app-landing (Static): 256MB 한도 / 128MB 예약
  • 사용 중: 2304MB 한도 / 1152MB 예약

게이트 절차 (이 순서대로, 건너뛰기 금지)

단계 G0: 서버 스냅샷 가져오기

항상 먼저 — 이것이 현재 서버 상태에 대한 진실입니다:

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

이것으로부터 도출:

  • TOTAL_RAM_MB (거의 항상 3840MB = 3.7GB)
  • COOLIFY_OVERHEAD_MB = 모든 Coolify 자체 컨테이너(coolify, coolify-db, coolify-redis, coolify-sentinel, coolify-proxy)의 현재 합계
  • RUNNING_PROJECTS_MB = 실행 중인 모든 프로젝트 컨테이너의 mem_limit 합계
  • AVAILABLE_MB = TOTAL_RAM_MB - 400 (OS) - COOLIFY_OVERHEAD_MB - RUNNING_PROJECTS_MB - 200 (안전)

단계 G1: 한도 계산 (AI 기반, 하드코드 금지)

프로젝트 유형에 따라 AI가 올바른 한도를 계산합니다:

프로젝트 유형 식별:
- Node.js / Next.js 앱       → BASE_MB = 350, WORKER_OVERHEAD = 워커당 50
- Python / Django / FastAPI   → BASE_MB = 200, WORKER_OVERHEAD = 워커당 30
- Go / Rust 서비스           → BASE_MB = 80,  WORKER_OVERHEAD = 워커당 20
- 정적 사이트 (nginx/caddy)   → BASE_MB = 64,  WORKER_OVERHEAD = 0
- Postgres                    → BASE_MB = 128, WORKER_OVERHEAD = 연결당 10
- Redis                       → BASE_MB = 64,  WORKER_OVERHEAD = 0
- 백그라운드 워커 (큐)      → BASE_MB = 200, WORKER_OVERHEAD = 워커당 50
- AI/LLM 서비스 (로컬)      → BASE_MB = 800+ (로컬 모델이면 최소 2GB)

한도 공식:

MEM_LIMIT_MB    = min(AVAILABLE_MB * 0.8, BASE_MB * 3)   -- 사용 가능의 80%, 최대 Base의 3배
MEM_RESERVATION = MEM_LIMIT_MB * 0.5                      -- 50%를 보장된 최소값으로
MEM_SWAP_MB     = MEM_LIMIT_MB * 1.4                      -- 40% 스왑 오버헤드 허용
CPU_LIMIT       = round(2.0 * (MEM_LIMIT_MB / AVAILABLE_MB), 1)  -- RAM 비율에 비례

NODE_OPTIONS_MB = floor(MEM_LIMIT_MB * 0.70)              -- V8 힙 = 컨테이너 한도의 70%
UV_THREADPOOL_SIZE = min(8, cpu_count * 2)                -- libuv 스레드

설정 전 검증 확인:

  • MEM_LIMIT_MB >= 128 (시작하기 위한 최소값)
  • AVAILABLE_MB - MEM_LIMIT_MB >= 200 (안전 버퍼 유지)
  • 모든 mem_limits의 합계 ≤ 3500MB (물리적 RAM의 85% 이상 오버커밋 금지)
  • 검증 실패 시 → 차단, Cem에게 알림

단계 G2: Coolify DB에 영구적으로 한도 설정

배포 회귀 문제에 대한 중요한 수정 사항입니다. Coolify는 모든 배포 시 자체 DB에서 docker-compose.yaml을 재생성합니다. 한도가 DB에 설정되어 있어야 배포 시에도 유지됩니다.

앱 UUID를 알고 있는 경우:

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

Coolify URL에서 UUID 가져오기: <COOLIFY_URL>/project/.../application/<UUID>/...

UUID를 모르는 경우 → DB를 통해 검색:

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

단계 G3: 환경 변수 설정

프로젝트의 .env 파일에 (echo >> .env 또는 Coolify UI를 통해):

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

# 워크플로우 앱용 (Activepieces 등):
AP_MAX_CONCURRENT_JOBS=5     # 샌드박스 폭발 방지
AP_TRIGGER_DEFAULT_POLL_INTERVAL=15  # DB 기본 부하 감소

# 모든 앱용:
# (프로덕션에서는 DEBUG 로깅 금지!)
NODE_ENV=production

중요: 줄바꿈 버그 방지! 각 변수가 별도 줄에 있는지 확인:

grep -c '^[A-Z_].*=' .env  # 변수 개수와 같아야 함
# 값 외에 한 줄에 두 개의 '='가 없어야 함
awk -F= 'NF>2 && $1 ~ /^[A-Z_]/' .env && echo "BUG: 줄에 중복 = 발견!"

단계 G4: Traefik Rate-Limiting 활성화

모든 새 공개 서비스에 대해:

<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이 자동으로 가져옵니다 (watch=true) — 재시작 불필요.

그런 다음 앱별 Traefik 구성에 미들웨어를 추가합니다:

middlewares:
  - rate-limit-strict

단계 G5: File Descriptor 한도 설정

Docker 데몬이 한도를 알아야 합니다. Gameboy 설정당 한 번, 영구적으로 유지:

<GAMEBOY_SSH> "
# 이미 설정되었는지 확인
grep -q 'nofile' /etc/docker/daemon.json && echo 'already set' || {
  # 백업 및 업데이트
  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')
  \"
  # 주의: systemctl restart docker는 모든 컨테이너를 재시작합니다!
  # 활성 트래픽이 없는 경우에만 → Cem과 조율
  echo 'NEEDS: systemctl restart docker (짧은 다운타임!)'
}
"

단계 G6: 부하 테스트 (실제 승인 심사)

동시 사용자 50명, 60초:

# 테스트 1: 기본 연결 가능성
curl -o /dev/null -s -w "HTTP=%{http_code} %{time_total}s\n" https://<APP_URL>/

# 테스트 2: 50개 동시 HTTP 요청 (30초)
/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%"

# 테스트 3: 부하 중 메모리 모니터링
<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

단계 G7: PASS/FAIL 기준

PASS (모두 충족되어야 함):

기준임계값
HTTP 상태2xx 또는 3xx (502/503/504 없음)
p99 대기 시간< 3000ms
메모리 증가안정적 (t=0과 t=60s 사이에 < +20%)
메모리가 한도 미만 유지컨테이너 한도의 < 90%
실패한 요청 없음< 0.1%
테스트 후 서버 연결 가능모든 URL에서 HTTP 200/302
테스트 후 CPU< 80% 지속

FAIL (하나라도 해당되면 차단):

  • 테스트 후 컨테이너가 중단/재시작됨
  • 메모리가 한도에 도달 (> 95%)
  • 실패한 요청 > 1%
  • p99 > 5000ms
  • Watchdog이 테스트 중 개입 (확인: <WATCHDOG_LOG>)

단계 G8: 게이트 결과

═══════════════════════════════════════════════════════
🎮 GAMEBOY-GATE 결과
═══════════════════════════════════════════════════════
프로젝트:     [이름]
앱-UUID:    [Coolify UUID]
테스트 시간:   [날짜/시간]

한도 계산:
  서버 RAM:              3840MB
  Coolify 오버헤드:        [n]MB (측정됨)
  다른 프로젝트:         [n]MB (한도 합계)
  사용 가능:               [n]MB
  할당됨:              MEM=[n]m, CPU=[n], NODE_OPTIONS=[n]MB

확인:
  ✅/❌ Coolify DB 한도 영구 설정됨
  ✅/❌ NODE_OPTIONS 올바름 (줄바꿈 버그 없음)
  ✅/❌ Rate-Limiting 활성화됨
  ✅/❌ File Descriptor 한도 65536
  ✅/❌ Redis maxmemory 정책 설정됨 (Redis인 경우)
  ✅/❌ 부하 테스트 50c/30s 통과

부하 테스트:
  Req/s:    [n]     (PASS if > 10)
  p50:      [n]ms
  p99:      [n]ms   (PASS if < 3000ms)
  실패:   [n]%    (PASS if < 0.1%)
  최대 메모리: [n]MB   (PASS if < 한도의 90%)

═══════════════════════════════════════════════════════
판정: ✅ PASS — 출시 허용
       ❌ FAIL — 출시 차단됨 ([이유])
═══════════════════════════════════════════════════════

빈번한 FAIL 원인 및 수정

FAIL: NODE_OPTIONS가 설정되지 않음

# 확인:
docker exec <container> printenv NODE_OPTIONS
# 수정:
grep -n 'NODE_OPTIONS' .env  # 줄바꿈 버그 확인
# 버그: HOST=0.0.0.0NODE_OPTIONS=... → 수정:
sed -i 's/HOST=0.0.0.0NODE_OPTIONS=/HOST=0.0.0.0\nNODE_OPTIONS=/' .env

FAIL: 테스트 후 컨테이너 OOM

근본 원인은 거의 항상 다음 중 하나:

  1. NODE_OPTIONS가 설정되지 않음 → V8이 컨테이너 RAM을 전부 사용
  2. 동시성 제한 없는 샌드박스 프로세스 (AP, 코드 실행)
  3. 앱 코드의 메모리 누수 (드물게)
# 재시작 발생 확인:
docker inspect <container> --format '{{.RestartCount}}'
# OOM 종료 확인:
dmesg | grep -i 'oom\|killed' | tail -10

FAIL: p99 > 3000ms

  1. 캐싱 없는 Heavy API 엔드포인트 → Redis 캐시 구현
  2. PG 연결 풀 고갈 → MAX_CONCURRENT_REQUESTS 또는 풀 크기 증가
  3. 요청당 Forward-Auth 오버헤드 → JWT 캐싱 구현

FAIL: 실패한 요청 > 0.1%

  1. Traefik 타임아웃이 앱 응답보다 짧음 → Traefik 타임아웃 증가
  2. FD 한도 고갈 → 단계 G5 (ulimits)
  3. PG max_connections 고갈 → PG에서 max_connections=200 설정

배포 복원력: 배포 생존 프로토콜

모든 Coolify 배포는 서버 충돌을 방지하기 위해 다음을 보장해야 합니다:

각 배포 전 확인:

<GAMEBOY_SSH> "
# 여유 RAM이 800MB 이상이어야 함
FREE=\$(free -m | awk '/^Mem:/{print \$7}')
echo "Available: \${FREE}MB"
[ \$FREE -lt 800 ] && echo '경고: 배포에 RAM 부족!' || echo '배포에 RAM OK'
# 디스크 여유 공간이 5GB 이상이어야 함 (이미지 풀에 공간 필요)
DISK=\$(df / | awk 'NR==2{print \$4}')
echo "Disk free: \$((DISK/1024))MB"
[ \$DISK -lt 5242880 ] && echo '경고: 디스크 부족!' || echo '디스크 OK'
"

Coolify가 배포 시 수행하는 작업 (그리고 우리가 보장해야 할 사항):

  1. Coolify가 DB를 읽음 → DB에 설정된 한도(포함)로 docker-compose.yaml 생성 (G2 → 영구 ✅)
  2. 컨테이너 중지 → 짧은 다운타임
  3. 새 컨테이너 시작 → Healthcheck가 앱 준비될 때까지 대기
  4. Traefik이 새 백엔드 엔드포인트를 받음

Healthcheck 구성 (Coolify UI 또는 DB에서 직접):

docker exec coolify-db psql -U coolify -d coolify -c "
UPDATE applications SET
  health_check_enabled = true,
  health_check_path = '/api/v1/flags',  -- 또는 '/', '/health', '/ping'
  health_check_port = '80',
  health_check_interval = 15,
  health_check_timeout = 5,
  health_check_retries = 3,
  health_check_start_period = 90  -- 무거운 앱의 경우 90초 시작 시간
WHERE uuid = '<APP_UUID>';
"

메모리 예산표 (2026년 6월 26일 기준)

GAMEBOY 4GB RAM — 예산 개요
════════════════════════════════════════════════════════════
시스템/OS:              400MB (고정)
Coolify 스택:          ~950MB (coolify+db+redis+sentinel+proxy)
────────────────────────────────────────────────────────────
프로젝트에 사용 가능: ~2650MB
════════════════════════════════════════════════════════════
app-workflow:          1536MB 한도 / 768MB 예약
app-billing:            512MB 한도 / 256MB 예약
app-landing:            256MB 한도 / 128MB 예약
────────────────────────────────────────────────────────────
아직 여유:             ~346MB 한도 여유
════════════════════════════════════════════════════════════

경고: Gameboy가 한도 용량의 약 91%를 사용 중입니다.
300MB 이상의 새 프로젝트는 리밸런싱 필요 (기존 
프로젝트 축소 또는 Coolify 스택 최적화).

새 프로젝트가 200MB 이상 필요한 경우 → 먼저 landing (256MB)을 128MB로 줄일 수 있는지 확인 (정적 사이트, RAM 적게 필요).


모든 배포를 위한 체크리스트 (빠른 참조)

배포 전:
  [ ] 여유 RAM > 800MB
  [ ] 디스크 > 5GB 여유
  [ ] Coolify DB 한도 설정됨 (0이 아님!)
  [ ] .env의 NODE_OPTIONS가 올바름 (줄바꿈 버그 없음)
  [ ] 공개 엔드포인트에 대해 Rate-Limiting 구성됨

배포 후:
  [ ] 컨테이너 실행 중 (docker ps → Up X seconds (healthy))
  [ ] URL에 연결 가능 (HTTP 2xx/3xx)
  [ ] NODE_OPTIONS 활성화 (docker exec <c> printenv NODE_OPTIONS)
  [ ] 메모리 안정적 (docker stats → 한도에 도달하지 않음)
  [ ] OOM 종료 없음 (dmesg | grep -i oom)

관련 스킬