🎮 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
근본 원인은 거의 항상 다음 중 하나:
NODE_OPTIONS가 설정되지 않음 → V8이 컨테이너 RAM을 전부 사용- 동시성 제한 없는 샌드박스 프로세스 (AP, 코드 실행)
- 앱 코드의 메모리 누수 (드물게)
# 재시작 발생 확인:
docker inspect <container> --format '{{.RestartCount}}'
# OOM 종료 확인:
dmesg | grep -i 'oom\|killed' | tail -10
FAIL: p99 > 3000ms
- 캐싱 없는 Heavy API 엔드포인트 → Redis 캐시 구현
- PG 연결 풀 고갈 →
MAX_CONCURRENT_REQUESTS또는 풀 크기 증가 - 요청당 Forward-Auth 오버헤드 → JWT 캐싱 구현
FAIL: 실패한 요청 > 0.1%
- Traefik 타임아웃이 앱 응답보다 짧음 → Traefik 타임아웃 증가
- FD 한도 고갈 → 단계 G5 (ulimits)
- 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가 배포 시 수행하는 작업 (그리고 우리가 보장해야 할 사항):
- Coolify가 DB를 읽음 → DB에 설정된 한도(포함)로 docker-compose.yaml 생성 (G2 → 영구 ✅)
- 컨테이너 중지 → 짧은 다운타임
- 새 컨테이너 시작 → Healthcheck가 앱 준비될 때까지 대기
- 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)